diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d620820..2ea8961f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,8 @@ name: Build and push DocsGPT Docker image on: - workflow_dispatch: - push: - branches: - - main + release: + types: [published] jobs: deploy: @@ -43,5 +41,7 @@ jobs: context: ./application push: true tags: | - ${{ secrets.DOCKER_USERNAME }}/docsgpt:latest - ghcr.io/${{ github.repository_owner }}/docsgpt:latest + ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }},${{ secrets.DOCKER_USERNAME }}/docsgpt:latest + ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }},ghcr.io/${{ github.repository_owner }}/docsgpt:latest + cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt:latest + cache-to: type=inline diff --git a/.github/workflows/cife.yml b/.github/workflows/cife.yml index 67aadfbb..73a97755 100644 --- a/.github/workflows/cife.yml +++ b/.github/workflows/cife.yml @@ -1,10 +1,8 @@ name: Build and push DocsGPT-FE Docker image on: - workflow_dispatch: - push: - branches: - - main + release: + types: [published] jobs: deploy: @@ -44,5 +42,7 @@ jobs: context: ./frontend push: true tags: | - ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest - ghcr.io/${{ github.repository_owner }}/docsgpt-fe:latest + ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }},${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest + ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }},ghcr.io/${{ github.repository_owner }}/docsgpt-fe:latest + cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest + cache-to: type=inline diff --git a/.github/workflows/docker-develop-build.yml b/.github/workflows/docker-develop-build.yml new file mode 100644 index 00000000..5edc69d7 --- /dev/null +++ b/.github/workflows/docker-develop-build.yml @@ -0,0 +1,49 @@ +name: Build and push DocsGPT Docker image for development + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + deploy: + if: github.repository == 'arc53/DocsGPT' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Login to ghcr.io + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker images to docker.io and ghcr.io + uses: docker/build-push-action@v4 + with: + file: './application/Dockerfile' + platforms: linux/amd64 + context: ./application + push: true + tags: | + ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop + ghcr.io/${{ github.repository_owner }}/docsgpt:develop + cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt:develop + cache-to: type=inline diff --git a/.github/workflows/docker-develop-fe-build.yml b/.github/workflows/docker-develop-fe-build.yml new file mode 100644 index 00000000..29ad4524 --- /dev/null +++ b/.github/workflows/docker-develop-fe-build.yml @@ -0,0 +1,49 @@ +name: Build and push DocsGPT FE Docker image for development + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + deploy: + if: github.repository == 'arc53/DocsGPT' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Login to ghcr.io + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker images to docker.io and ghcr.io + uses: docker/build-push-action@v4 + with: + file: './frontend/Dockerfile' + platforms: linux/amd64 + context: ./frontend + push: true + tags: | + ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop + ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop + cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop + cache-to: type=inline diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5200794b..1b0567e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Thank you for choosing to contribute to DocsGPT! We are all very grateful! 📣 **Discussions** - Engage in conversations, start new topics, or help answer questions. -🐞 **Issues** - This is where we keep track of tasks. It could be bugs,fixes or suggestions for new features. +🐞 **Issues** - This is where we keep track of tasks. It could be bugs, fixes or suggestions for new features. 🛠️ **Pull requests** - Suggest changes to our repository, either by working on existing issues or adding new features. @@ -21,8 +21,9 @@ Thank you for choosing to contribute to DocsGPT! We are all very grateful! - If you're interested in contributing code, here are some important things to know: - We have a frontend built on React (Vite) and a backend in Python. -======= -Before creating issues, please check out how the latest version of our app looks and works by launching it via [Quickstart](https://github.com/arc53/DocsGPT#quickstart) the version on our live demo is slightly modified with login. Your issues should relate to the version that you can launch via [Quickstart](https://github.com/arc53/DocsGPT#quickstart). + + +Before creating issues, please check out how the latest version of our app looks and works by launching it via [Quickstart](https://github.com/arc53/DocsGPT#quickstart) the version on our live demo is slightly modified with login. Your issues should relate to the version you can launch via [Quickstart](https://github.com/arc53/DocsGPT#quickstart). ### 👨‍💻 If you're interested in contributing code, here are some important things to know: @@ -43,7 +44,7 @@ Please try to follow the guidelines. ### 🖥 If you are looking to contribute to Backend (🐍 Python): -- Review our issues and contribute to [`/application`](https://github.com/arc53/DocsGPT/tree/main/application) or [`/scripts`](https://github.com/arc53/DocsGPT/tree/main/scripts) (please disregard old [`ingest_rst.py`](https://github.com/arc53/DocsGPT/blob/main/scripts/old/ingest_rst.py) [`ingest_rst_sphinx.py`](https://github.com/arc53/DocsGPT/blob/main/scripts/old/ingest_rst_sphinx.py) files; they will be deprecated soon). +- Review our issues and contribute to [`/application`](https://github.com/arc53/DocsGPT/tree/main/application) or [`/scripts`](https://github.com/arc53/DocsGPT/tree/main/scripts) (please disregard old [`ingest_rst.py`](https://github.com/arc53/DocsGPT/blob/main/scripts/old/ingest_rst.py) [`ingest_rst_sphinx.py`](https://github.com/arc53/DocsGPT/blob/main/scripts/old/ingest_rst_sphinx.py) files; these will be deprecated soon). - All new code should be covered with unit tests ([pytest](https://github.com/pytest-dev/pytest)). Please find tests under [`/tests`](https://github.com/arc53/DocsGPT/tree/main/tests) folder. - Before submitting your Pull Request, ensure it can be queried after ingesting some test data. @@ -125,4 +126,4 @@ Thank you for considering contributing to DocsGPT! 🙏 ## Questions/collaboration Feel free to join our [Discord](https://discord.gg/n5BX8dh8rU). We're very friendly and welcoming to new contributors, so don't hesitate to reach out. -# Thank you so much for considering to contribute DocsGPT!🙏 +# Thank you so much for considering to contributing DocsGPT!🙏 diff --git a/HACKTOBERFEST.md b/HACKTOBERFEST.md index 47679960..8656bd84 100644 --- a/HACKTOBERFEST.md +++ b/HACKTOBERFEST.md @@ -4,7 +4,7 @@ Welcome, contributors! We're excited to announce that DocsGPT is participating i All contributors with accepted PRs will receive a cool Holopin! 🤩 (Watch out for a reply in your PR to collect it). -### 🏆 Top 50 contributors will recieve a special T-shirt +### 🏆 Top 50 contributors will receive a special T-shirt ### 🏆 [LLM Document analysis by LexEU competition](https://github.com/arc53/DocsGPT/blob/main/lexeu-competition.md): A separate competition is available for those who submit new retrieval / workflow method that will analyze a Document using EU laws. @@ -16,14 +16,14 @@ You can find more information [here](https://github.com/arc53/DocsGPT/blob/main/ 🛠️ Code: This is the golden ticket! Make meaningful contributions through PRs. 🧩 API extension: Build an app utilising DocsGPT API. We prefer submissions that showcase original ideas and turn the API into an AI agent. -They can be a completely separate repo. +They can be a completely separate repos. For example: https://github.com/arc53/tg-bot-docsgpt-extenstion or https://github.com/arc53/DocsGPT-cli Non-Code Contributions: -📚 Wiki: Improve our documentation, Create a guide or change existing documentation. +📚 Wiki: Improve our documentation, create a guide or change existing documentation. 🖥️ Design: Improve the UI/UX or design a new feature. @@ -37,5 +37,5 @@ Non-Code Contributions: - Refer to the [Documentation](https://docs.docsgpt.cloud/). - Feel free to join our [Discord](https://discord.gg/n5BX8dh8rU) server. We're here to help newcomers, so don't hesitate to jump in! Join us [here](https://discord.gg/n5BX8dh8rU). -Thank you very much for considering contributing to DocsGPT during Hacktoberfest! 🙏 Your contributions (not just simple typo) could earn you a stylish new t-shirt and other prizes as a token of our appreciation. 🎁 Join us, and let's code together! 🚀 +Thank you very much for considering contributing to DocsGPT during Hacktoberfest! 🙏 Your contributions (not just simple typos) could earn you a stylish new t-shirt and other prizes as a token of our appreciation. 🎁 Join us, and let's code together! 🚀 diff --git a/README.md b/README.md index f1942dc1..ee9a1af6 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,8 @@ We're eager to provide personalized assistance when deploying your DocsGPT to a [Send Email :email:](mailto:contact@arc53.com?subject=DocsGPT%20support%2Fsolutions) -![video-example-of-docs-gpt](https://d3dg1063dc54p9.cloudfront.net/videos/demov3.gif) + +video-example-of-docs-gpt ## Roadmap diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 9a22db84..17eb5cc3 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -292,6 +292,7 @@ class Stream(Resource): def post(self): data = request.get_json() required_fields = ["question"] + missing_fields = check_required_fields(data, required_fields) if missing_fields: return missing_fields @@ -422,7 +423,7 @@ class Answer(Resource): @api.doc(description="Provide an answer based on the question and retriever") def post(self): data = request.get_json() - required_fields = ["question"] + required_fields = ["question"] missing_fields = check_required_fields(data, required_fields) if missing_fields: return missing_fields diff --git a/application/cache.py b/application/cache.py new file mode 100644 index 00000000..33022e45 --- /dev/null +++ b/application/cache.py @@ -0,0 +1,93 @@ +import redis +import time +import json +import logging +from threading import Lock +from application.core.settings import settings +from application.utils import get_hash + +logger = logging.getLogger(__name__) + +_redis_instance = None +_instance_lock = Lock() + +def get_redis_instance(): + global _redis_instance + if _redis_instance is None: + with _instance_lock: + if _redis_instance is None: + try: + _redis_instance = redis.Redis.from_url(settings.CACHE_REDIS_URL, socket_connect_timeout=2) + except redis.ConnectionError as e: + logger.error(f"Redis connection error: {e}") + _redis_instance = None + return _redis_instance + +def gen_cache_key(*messages, model="docgpt"): + if not all(isinstance(msg, dict) for msg in messages): + raise ValueError("All messages must be dictionaries.") + messages_str = json.dumps(list(messages), sort_keys=True) + combined = f"{model}_{messages_str}" + cache_key = get_hash(combined) + return cache_key + +def gen_cache(func): + def wrapper(self, model, messages, *args, **kwargs): + try: + cache_key = gen_cache_key(*messages) + redis_client = get_redis_instance() + if redis_client: + try: + cached_response = redis_client.get(cache_key) + if cached_response: + return cached_response.decode('utf-8') + except redis.ConnectionError as e: + logger.error(f"Redis connection error: {e}") + + result = func(self, model, messages, *args, **kwargs) + if redis_client: + try: + redis_client.set(cache_key, result, ex=1800) + except redis.ConnectionError as e: + logger.error(f"Redis connection error: {e}") + + return result + except ValueError as e: + logger.error(e) + return "Error: No user message found in the conversation to generate a cache key." + return wrapper + +def stream_cache(func): + def wrapper(self, model, messages, stream, *args, **kwargs): + cache_key = gen_cache_key(*messages) + logger.info(f"Stream cache key: {cache_key}") + + redis_client = get_redis_instance() + if redis_client: + try: + cached_response = redis_client.get(cache_key) + if cached_response: + logger.info(f"Cache hit for stream key: {cache_key}") + cached_response = json.loads(cached_response.decode('utf-8')) + for chunk in cached_response: + yield chunk + time.sleep(0.03) + return + except redis.ConnectionError as e: + logger.error(f"Redis connection error: {e}") + + result = func(self, model, messages, stream, *args, **kwargs) + stream_cache_data = [] + + for chunk in result: + stream_cache_data.append(chunk) + yield chunk + + if redis_client: + try: + redis_client.set(cache_key, json.dumps(stream_cache_data), ex=1800) + logger.info(f"Stream cache saved for key: {cache_key}") + except redis.ConnectionError as e: + logger.error(f"Redis connection error: {e}") + + return wrapper \ No newline at end of file diff --git a/application/core/settings.py b/application/core/settings.py index e6173be4..7346da08 100644 --- a/application/core/settings.py +++ b/application/core/settings.py @@ -21,6 +21,9 @@ class Settings(BaseSettings): VECTOR_STORE: str = "faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus" RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search + # LLM Cache + CACHE_REDIS_URL: str = "redis://localhost:6379/2" + API_URL: str = "http://localhost:7091" # backend url for celery worker API_KEY: Optional[str] = None # LLM api key diff --git a/application/llm/base.py b/application/llm/base.py index 475b7937..1caab5d3 100644 --- a/application/llm/base.py +++ b/application/llm/base.py @@ -1,28 +1,29 @@ from abc import ABC, abstractmethod from application.usage import gen_token_usage, stream_token_usage +from application.cache import stream_cache, gen_cache class BaseLLM(ABC): def __init__(self): self.token_usage = {"prompt_tokens": 0, "generated_tokens": 0} - def _apply_decorator(self, method, decorator, *args, **kwargs): - return decorator(method, *args, **kwargs) + def _apply_decorator(self, method, decorators, *args, **kwargs): + for decorator in decorators: + method = decorator(method) + return method(self, *args, **kwargs) @abstractmethod def _raw_gen(self, model, messages, stream, *args, **kwargs): pass def gen(self, model, messages, stream=False, *args, **kwargs): - return self._apply_decorator(self._raw_gen, gen_token_usage)( - self, model=model, messages=messages, stream=stream, *args, **kwargs - ) + decorators = [gen_token_usage, gen_cache] + return self._apply_decorator(self._raw_gen, decorators=decorators, model=model, messages=messages, stream=stream, *args, **kwargs) @abstractmethod def _raw_gen_stream(self, model, messages, stream, *args, **kwargs): pass def gen_stream(self, model, messages, stream=True, *args, **kwargs): - return self._apply_decorator(self._raw_gen_stream, stream_token_usage)( - self, model=model, messages=messages, stream=stream, *args, **kwargs - ) + decorators = [stream_cache, stream_token_usage] + return self._apply_decorator(self._raw_gen_stream, decorators=decorators, model=model, messages=messages, stream=stream, *args, **kwargs) \ No newline at end of file diff --git a/application/requirements.txt b/application/requirements.txt index 6a57dd12..6ea1d1ba 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -4,7 +4,7 @@ beautifulsoup4==4.12.3 celery==5.3.6 dataclasses-json==0.6.7 docx2txt==0.8 -duckduckgo-search==6.2.6 +duckduckgo-search==6.3.0 ebooklib==0.18 elastic-transport==8.15.0 elasticsearch==8.15.1 @@ -54,7 +54,7 @@ pathable==0.4.3 pillow==10.4.0 portalocker==2.10.1 prance==23.6.21.0 -primp==0.6.2 +primp==0.6.3 prompt-toolkit==3.0.47 protobuf==5.28.2 py==1.11.0 diff --git a/application/utils.py b/application/utils.py index f0802c39..1fc9e329 100644 --- a/application/utils.py +++ b/application/utils.py @@ -1,6 +1,8 @@ import tiktoken +import hashlib from flask import jsonify, make_response + _encoding = None @@ -39,3 +41,8 @@ def check_required_fields(data, required_fields): 400, ) return None + + +def get_hash(data): + return hashlib.md5(data.encode()).hexdigest() + diff --git a/docker-compose.yaml b/docker-compose.yaml index f3b8a363..d3f3421a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -20,6 +20,7 @@ services: - CELERY_BROKER_URL=redis://redis:6379/0 - CELERY_RESULT_BACKEND=redis://redis:6379/1 - MONGO_URI=mongodb://mongo:27017/docsgpt + - CACHE_REDIS_URL=redis://redis:6379/2 ports: - "7091:7091" volumes: @@ -41,6 +42,7 @@ services: - CELERY_RESULT_BACKEND=redis://redis:6379/1 - MONGO_URI=mongodb://mongo:27017/docsgpt - API_URL=http://backend:7091 + - CACHE_REDIS_URL=redis://redis:6379/2 depends_on: - redis - mongo diff --git a/docs/README.md b/docs/README.md index 4b90b598..12ebbf08 100644 --- a/docs/README.md +++ b/docs/README.md @@ -46,6 +46,6 @@ yarn install yarn dev ``` -- Now, you should be able to view the docs on your local environment by visiting `http://localhost:5000`. You can explore the different markdown files and make changes as you see fit. +- Now, you should be able to view the docs on your local environment by visiting `http://localhost:3000`. You can explore the different markdown files and make changes as you see fit. - **Footnotes:** This guide assumes you have Node.js and npm installed. The guide involves running a local server using yarn, and viewing the documentation offline. If you encounter any issues, it may be worth verifying your Node.js and npm installations and whether you have installed yarn correctly. diff --git a/docs/pages/Guides/How-to-train-on-other-documentation.mdx b/docs/pages/Guides/How-to-train-on-other-documentation.mdx index e5429a04..f0149618 100644 --- a/docs/pages/Guides/How-to-train-on-other-documentation.mdx +++ b/docs/pages/Guides/How-to-train-on-other-documentation.mdx @@ -28,15 +28,15 @@ Navigate to the sidebar where you will find `Source Docs` option,here you will f ### Step 2 -Click on the `Upload icon` just beside the source docs options,now borwse and upload the document which you want to train on or select the `remote` option if you have to insert the link of the documentation. +Click on the `Upload icon` just beside the source docs options,now browse and upload the document which you want to train on or select the `remote` option if you have to insert the link of the documentation. ### Step 3 -Now you will be able to see the name of the file uploaded under the Uploaded Files ,now click on `Train`,once you click on train it might take some time to train on the document. You will be able to see the `Training progress` and once the training is completed you can click the `finish` button and there you go your docuemnt is uploaded. +Now you will be able to see the name of the file uploaded under the Uploaded Files ,now click on `Train`,once you click on train it might take some time to train on the document. You will be able to see the `Training progress` and once the training is completed you can click the `finish` button and there you go your document is uploaded. ### Step 4 -Go to `New chat` and from the side bar select the document you uploaded under the `Source Docs` and go ahead with your chat, now you can ask qestions regarding the document you uploaded and you will get the effective answer based on it. +Go to `New chat` and from the side bar select the document you uploaded under the `Source Docs` and go ahead with your chat, now you can ask questions regarding the document you uploaded and you will get the effective answer based on it. diff --git a/docs/pages/Guides/How-to-use-different-LLM.mdx b/docs/pages/Guides/How-to-use-different-LLM.mdx index 7df77742..c867fdcc 100644 --- a/docs/pages/Guides/How-to-use-different-LLM.mdx +++ b/docs/pages/Guides/How-to-use-different-LLM.mdx @@ -33,7 +33,7 @@ For open source you have to edit .env file with LLM_NAME with their desired LLM All the supported LLM providers are here application/llm and you can check what env variable are needed for each List of latest supported LLMs are https://github.com/arc53/DocsGPT/blob/main/application/llm/llm_creator.py ### Step 3 -Visit application/llm and select the file of your selected llm and there you will find the speicifc requirements needed to be filled in order to use it,i.e API key of that llm. +Visit application/llm and select the file of your selected llm and there you will find the specific requirements needed to be filled in order to use it,i.e API key of that llm. ### For OpenAI-Compatible Endpoints: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4087e4f5..75f4ea8e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1675,7 +1675,7 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "devOptional": true, + "dev": true, "dependencies": { "@types/react": "*" } diff --git a/frontend/signal-desktop-keyring.gpg b/frontend/signal-desktop-keyring.gpg new file mode 100644 index 00000000..b5e68a04 Binary files /dev/null and b/frontend/signal-desktop-keyring.gpg differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e1157141..176ae518 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,7 +32,10 @@ function MainLayout() { } export default function App() { - useDarkTheme(); + const [, , componentMounted] = useDarkTheme(); + if (!componentMounted) { + return
; + } return (
diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index 3a55525a..3d1dc614 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -11,7 +11,6 @@ import Discord from './assets/discord.svg'; import Expand from './assets/expand.svg'; import Github from './assets/github.svg'; import Hamburger from './assets/hamburger.svg'; -import Info from './assets/info.svg'; import SettingGear from './assets/settingGear.svg'; import Twitter from './assets/TwitterX.svg'; import UploadIcon from './assets/upload.svg'; @@ -41,6 +40,7 @@ import { setSourceDocs, } from './preferences/preferenceSlice'; import Upload from './upload/Upload'; +import Help from './components/Help'; interface NavigationProps { navOpen: boolean; @@ -275,7 +275,10 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { {t('newChat')}

-
+
{conversations && conversations.length > 0 ? (
@@ -304,7 +307,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { <> )}
-
@@ -359,68 +361,51 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {

-
- { - if (isMobile) { - setNavOpen(!navOpen); - } - resetConversation(); - }} - to="/about" - className={({ isActive }) => - `my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${ - isActive ? 'bg-gray-3000 dark:bg-[#28292E]' : '' - }` - } - > - icon -

{t('about')}

-
-
- - discord - - - x - - - github - +
+
+ + +
+ + discord + + + x + + + github + +
@@ -450,6 +435,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { ); diff --git a/frontend/src/assets/documentation-dark.svg b/frontend/src/assets/documentation-dark.svg index 78440206..5cbde1b1 100644 --- a/frontend/src/assets/documentation-dark.svg +++ b/frontend/src/assets/documentation-dark.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/frontend/src/assets/documentation.svg b/frontend/src/assets/documentation.svg index f9f7c596..955d392f 100644 --- a/frontend/src/assets/documentation.svg +++ b/frontend/src/assets/documentation.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/frontend/src/assets/envelope-dark.svg b/frontend/src/assets/envelope-dark.svg new file mode 100644 index 00000000..a61bec4f --- /dev/null +++ b/frontend/src/assets/envelope-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/envelope.svg b/frontend/src/assets/envelope.svg new file mode 100644 index 00000000..a4c25032 --- /dev/null +++ b/frontend/src/assets/envelope.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/file_upload.svg b/frontend/src/assets/file_upload.svg new file mode 100644 index 00000000..f48d8d81 --- /dev/null +++ b/frontend/src/assets/file_upload.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/assets/website_collect.svg b/frontend/src/assets/website_collect.svg new file mode 100644 index 00000000..b7aa60cf --- /dev/null +++ b/frontend/src/assets/website_collect.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/components/Help.tsx b/frontend/src/components/Help.tsx new file mode 100644 index 00000000..0112a805 --- /dev/null +++ b/frontend/src/components/Help.tsx @@ -0,0 +1,80 @@ +import { useState, useRef, useEffect } from 'react'; +import Info from '../assets/info.svg'; +import PageIcon from '../assets/documentation.svg'; +import EmailIcon from '../assets/envelope.svg'; +import { useTranslation } from 'react-i18next'; +const Help = () => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + const buttonRef = useRef(null); + const { t } = useTranslation(); + + const toggleDropdown = () => { + setIsOpen((prev) => !prev); + }; + + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) && + buttonRef.current && + !buttonRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( +
+ + {isOpen && ( + + )} +
+ ); +}; + +export default Help; diff --git a/frontend/src/components/RetryIcon.tsx b/frontend/src/components/RetryIcon.tsx index 27ed6028..8cecbd2f 100644 --- a/frontend/src/components/RetryIcon.tsx +++ b/frontend/src/components/RetryIcon.tsx @@ -4,10 +4,11 @@ const RetryIcon = (props: SVGProps) => ( diff --git a/frontend/src/components/SettingsBar.tsx b/frontend/src/components/SettingsBar.tsx index 2b7c2a33..f617c6e8 100644 --- a/frontend/src/components/SettingsBar.tsx +++ b/frontend/src/components/SettingsBar.tsx @@ -71,9 +71,9 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => { alert -
- {retryBtn} -
- - )}
+ {type === 'ERROR' && ( +
+
{retryBtn}
+
+ )} {handleFeedback && ( <>
{ + event.preventDefault(); + }, []); + + useEffect(() => { + const conversationsMainDiv = document.getElementById( + 'conversationsMainDiv', + ); + + if (conversationsMainDiv) { + if (isOpen) { + conversationsMainDiv.addEventListener('wheel', preventScroll, { + passive: false, + }); + conversationsMainDiv.addEventListener('touchmove', preventScroll, { + passive: false, + }); + } else { + conversationsMainDiv.removeEventListener('wheel', preventScroll); + conversationsMainDiv.removeEventListener('touchmove', preventScroll); + } + + return () => { + conversationsMainDiv.removeEventListener('wheel', preventScroll); + conversationsMainDiv.removeEventListener('touchmove', preventScroll); + }; + } + }, [isOpen]); + function onClear() { setConversationsName(conversation.name); setIsEdit(false); @@ -96,17 +130,13 @@ export default function ConversationTile({ conversationId !== conversation.id && selectConversation(conversation.id); }} - className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${ + className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between pl-4 gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${ conversationId === conversation.id || isOpen || isHovered ? 'bg-gray-100 dark:bg-[#28292E]' : '' }`} >
- {isEdit ? ( { event.stopPropagation(); - setOpen(true); + setOpen(!isOpen); }} className="mr-2 flex w-4 justify-center" > diff --git a/frontend/src/conversation/conversationHandlers.ts b/frontend/src/conversation/conversationHandlers.ts index 338ed864..e28e4b22 100644 --- a/frontend/src/conversation/conversationHandlers.ts +++ b/frontend/src/conversation/conversationHandlers.ts @@ -38,9 +38,11 @@ export function handleFetchAnswer( prompt_id: promptId, chunks: chunks, token_limit: token_limit, + isNoneDoc: selectedDocs === null, }; - if (selectedDocs && 'id' in selectedDocs) + if (selectedDocs && 'id' in selectedDocs) { payload.active_docs = selectedDocs.id as string; + } payload.retriever = selectedDocs?.retriever as string; return conversationService .answer(payload, signal) @@ -84,26 +86,16 @@ export function handleFetchAnswerSteaming( prompt_id: promptId, chunks: chunks, token_limit: token_limit, + isNoneDoc: selectedDocs === null, }; - if (selectedDocs && 'id' in selectedDocs) + if (selectedDocs && 'id' in selectedDocs) { payload.active_docs = selectedDocs.id as string; + } payload.retriever = selectedDocs?.retriever as string; return new Promise((resolve, reject) => { conversationService - .answerStream( - { - question: question, - active_docs: selectedDocs?.id as string, - history: JSON.stringify(history), - conversation_id: conversationId, - prompt_id: promptId, - chunks: chunks, - token_limit: token_limit, - isNoneDoc: selectedDocs === null, - }, - signal, - ) + .answerStream(payload, signal) .then((response) => { if (!response.body) throw Error('No response body'); @@ -169,20 +161,13 @@ export function handleSearch( conversation_id: conversation_id, chunks: chunks, token_limit: token_limit, + isNoneDoc: selectedDocs === null, }; if (selectedDocs && 'id' in selectedDocs) payload.active_docs = selectedDocs.id as string; payload.retriever = selectedDocs?.retriever as string; return conversationService - .search({ - question: question, - active_docs: selectedDocs?.id as string, - conversation_id, - history, - chunks: chunks, - token_limit: token_limit, - isNoneDoc: selectedDocs === null, - }) + .search(payload) .then((response) => response.json()) .then((data) => { return data; diff --git a/frontend/src/conversation/conversationModels.ts b/frontend/src/conversation/conversationModels.ts index bf86678b..99bb69e6 100644 --- a/frontend/src/conversation/conversationModels.ts +++ b/frontend/src/conversation/conversationModels.ts @@ -40,4 +40,5 @@ export interface RetrievalPayload { prompt_id?: string | null; chunks: string; token_limit: number; + isNoneDoc: boolean; } diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index dfb9cb46..b993f3da 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -82,6 +82,7 @@ export function useDarkTheme() { }; const [isDarkTheme, setIsDarkTheme] = useState(getInitialTheme()); + const [componentMounted, setComponentMounted] = useState(false); useEffect(() => { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); @@ -102,11 +103,12 @@ export function useDarkTheme() { } else { document.body?.classList.remove('dark'); } + setComponentMounted(true); }, [isDarkTheme]); const toggleTheme = () => { setIsDarkTheme(!isDarkTheme); }; - return [isDarkTheme, toggleTheme] as const; + return [isDarkTheme, toggleTheme, componentMounted] as const; } diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index f5b48d75..335d1e6c 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -7,9 +7,12 @@ "about": "About", "inputPlaceholder": "Type your message here...", "tagline": "DocsGPT uses GenAI, please review critical information using sources.", - "sourceDocs": "Source Docs", + "sourceDocs": "Source", "none": "None", "cancel": "Cancel", + "help":"Help", + "emailUs":"Email us", + "documentation":"documentation", "demo": [ { "header": "Learn about DocsGPT", @@ -75,8 +78,12 @@ "modals": { "uploadDoc": { "label": "Upload New Documentation", - "file": "From File", - "remote": "Remote", + "select": "Choose how to upload your document to DocsGPT", + "file": "Upload from device", + "back": "Back", + "wait": "Please wait ...", + "remote": "Collect from a website", + "start": "Start Chatting", "name": "Name", "choose": "Choose Files", "info": "Please upload .pdf, .txt, .rst, .csv, .xlsx, .docx, .md, .zip limited to 25mb", diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index 98b38d7c..9358aafa 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -7,9 +7,12 @@ "about": "Acerca de", "inputPlaceholder": "Escribe tu mensaje aquí...", "tagline": "DocsGPT utiliza GenAI, por favor revisa información crítica utilizando fuentes.", - "sourceDocs": "Documentos Fuente", + "sourceDocs": "Fuente", "none": "Nada", "cancel": "Cancelar", + "help":"Asistencia", + "emailUs": "Envíanos un correo", + "documentation": "documentación", "demo": [ { "header": "Aprende sobre DocsGPT", @@ -75,8 +78,12 @@ "modals": { "uploadDoc": { "label": "Subir Nueva Documentación", - "file": "Desde Archivo", - "remote": "Remota", + "select": "Elija cómo cargar su documento en DocsGPT", + "file": "Subir desde el dispositivo", + "back": "Atrás", + "wait": "Espere por favor ...", + "remote": "Recoger desde un sitio web", + "start": "Empezar a chatear", "name": "Nombre", "choose": "Seleccionar Archivos", "info": "Por favor, suba archivos .pdf, .txt, .rst, .docx, .md, .zip limitados a 25 MB", diff --git a/frontend/src/locale/i18n.ts b/frontend/src/locale/i18n.ts index dbf5ae1b..72f9ec28 100644 --- a/frontend/src/locale/i18n.ts +++ b/frontend/src/locale/i18n.ts @@ -6,6 +6,7 @@ import en from './en.json'; //English import es from './es.json'; //Spanish import jp from './jp.json'; //Japanese import zh from './zh.json'; //Mandarin +import zhTW from './zh-TW.json'; //Traditional Chinese i18n .use(LanguageDetector) @@ -24,6 +25,9 @@ i18n zh: { translation: zh, }, + 'zh-TW': { + translation: zhTW, + }, }, fallbackLng: 'en', detection: { diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index b34cc5e5..2adc4947 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -7,9 +7,12 @@ "about": "について", "inputPlaceholder": "ここにメッセージを入力してください...", "tagline": "DocsGPTはGenAIを使用しています。重要な情報はソースで確認してください。", - "sourceDocs": "ソースドキュメント", + "sourceDocs": "ソース", "none": "なし", "cancel": "キャンセル", + "help":"ヘルプ", + "emailUs": "メールを送る", + "documentation": "ドキュメント", "demo": [ { "header": "DocsGPTについて学ぶ", @@ -75,8 +78,12 @@ "modals": { "uploadDoc": { "label": "新規書類のアップロード", - "file": "ファイルから", - "remote": "リモート", + "select": "ドキュメントを DocsGPT にアップロードする方法を選択します", + "file": "デバイスからアップロード", + "back": "戻る", + "wait": "お待ちください ...", + "remote": "ウェブサイトから収集する", + "start": "チャットを開始する", "name": "名前", "choose": "ファイルを選択", "info": ".pdf, .txt, .rst, .docx, .md, .zipファイルを25MBまでアップロードしてください", diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json new file mode 100644 index 00000000..a826baed --- /dev/null +++ b/frontend/src/locale/zh-TW.json @@ -0,0 +1,133 @@ +{ + "language": "繁體中文(臺灣)", + "chat": "對話", + "chats": "對話", + "newChat": "新對話", + "myPlan": "我的方案", + "about": "關於", + "inputPlaceholder": "在此輸入您的訊息...", + "tagline": "DocsGPT 使用生成式 AI,請使用原始資料來源審閱重要資訊。", + "sourceDocs": "原始文件", + "none": "無", + "cancel": "取消", + "help":"聯繫支援", + "emailUs": "寄送電子郵件給我們", + "documentation": "文件", + "demo": [ + { + "header": "了解 DocsGPT", + "query": "什麼是 DocsGPT?" + }, + { + "header": "摘要文件", + "query": "摘要目前的內容" + }, + { + "header": "撰寫程式碼", + "query": "為 /api/answer 撰寫 API 請求程式碼" + }, + { + "header": "學習輔助", + "query": "為此內容撰寫可能的問題" + } + ], + "settings": { + "label": "設定", + "general": { + "label": "一般", + "selectTheme": "選擇主題", + "light": "淺色", + "dark": "深色", + "selectLanguage": "選擇語言", + "chunks": "每次查詢處理的區塊數", + "prompt": "使用中的提示", + "deleteAllLabel": "刪除所有對話", + "deleteAllBtn": "全部刪除", + "addNew": "新增", + "convHistory": "對話歷史記錄", + "none": "無", + "low": "低", + "medium": "中", + "high": "高", + "unlimited": "無限制", + "default": "預設" + }, + "documents": { + "label": "文件", + "name": "文件名稱", + "date": "向量日期", + "type": "類型", + "tokenUsage": "Token 使用量" + }, + "apiKeys": { + "label": "API 金鑰", + "name": "名稱", + "key": "API 金鑰", + "sourceDoc": "來源文件", + "createNew": "新增" + }, + "analytics": { + "label": "分析" + }, + "logs": { + "label": "日誌" + } + }, + "modals": { + "uploadDoc": { + "label": "上傳新文件", + "file": "從檔案", + "remote": "遠端", + "name": "名稱", + "choose": "選擇檔案", + "info": "請上傳 .pdf, .txt, .rst, .docx, .md, .zip 檔案,大小限制為 25MB", + "uploadedFiles": "已上傳的檔案", + "cancel": "取消", + "train": "訓練", + "link": "連結", + "urlLink": "URL 連結", + "reddit": { + "id": "用戶端 ID", + "secret": "用戶端金鑰", + "agent": "使用者代理(User-Agent)", + "searchQueries": "搜尋查詢", + "numberOfPosts": "貼文數量" + } + }, + "createAPIKey": { + "label": "建立新的 API 金鑰", + "apiKeyName": "API 金鑰名稱", + "chunks": "每次查詢處理的區塊數", + "prompt": "選擇使用中的提示", + "sourceDoc": "來源文件", + "create": "建立" + }, + "saveKey": { + "note": "請儲存您的金鑰", + "disclaimer": "這是唯一一次顯示您的金鑰。", + "copy": "複製", + "copied": "已複製", + "confirm": "我已儲存金鑰" + }, + "deleteConv": { + "confirm": "您確定要刪除所有對話嗎?", + "delete": "刪除" + }, + "shareConv": { + "label": "建立公開頁面以分享", + "note": "來源文件、個人資訊和後續對話將保持私密", + "create": "建立" + } + }, + "sharedConv": { + "subtitle": "使用以下工具建立", + "button": "開始使用 DocsGPT", + "meta": "DocsGPT 使用生成式 AI,請使用原始資料來源審閱重要資訊。" + }, + "convTile": { + "share": "分享", + "delete": "刪除", + "rename": "重新命名", + "deleteWarning": "您確定要刪除這個對話嗎?" + } +} diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index 7decdefe..427bddc4 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -7,9 +7,12 @@ "about": "关于", "inputPlaceholder": "在这里输入您的消息...", "tagline": "DocsGPT 使用 GenAI, 请使用来源审核关键信息.", - "sourceDocs": "来源文档", + "sourceDocs": "源", "none": "无", "cancel": "取消", + "help":"联系支持", + "emailUs": "给我们发邮件", + "documentation": "文档", "demo": [ { "header": "了解 DocsGPT", @@ -75,8 +78,12 @@ "modals": { "uploadDoc": { "label": "上传新文档资料", - "file": "从文件", - "remote": "远程", + "select": "选择如何将文档上传到 DocsGPT", + "file": "从设备上传", + "back": "后退", + "wait": "请稍等 ...", + "remote": "从网站收集", + "start": "开始聊天", "name": "名称", "choose": "选择文件", "info": "请上传 .pdf, .txt, .rst, .docx, .md, .zip 文件,限 25MB", diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index 07370541..e230f272 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -6,7 +6,7 @@ import Trash from '../assets/trash.svg'; import CreateAPIKeyModal from '../modals/CreateAPIKeyModal'; import SaveAPIKeyModal from '../modals/SaveAPIKeyModal'; import { APIKeyData } from './types'; -import SkeletonLoader from '../utils/loader'; +import SkeletonLoader from '../components/SkeletonLoader'; export default function APIKeys() { const { t } = useTranslation(); diff --git a/frontend/src/settings/Analytics.tsx b/frontend/src/settings/Analytics.tsx index 6103c35e..8baad361 100644 --- a/frontend/src/settings/Analytics.tsx +++ b/frontend/src/settings/Analytics.tsx @@ -17,7 +17,7 @@ import { formatDate } from '../utils/dateTimeUtils'; import { APIKeyData } from './types'; import type { ChartData } from 'chart.js'; -import SkeletonLoader from '../utils/loader'; +import SkeletonLoader from '../components/SkeletonLoader'; ChartJS.register( CategoryScale, diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index fd7f1165..867fc9de 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -7,7 +7,7 @@ import userService from '../api/services/userService'; import SyncIcon from '../assets/sync.svg'; import Trash from '../assets/trash.svg'; import DropdownMenu from '../components/DropdownMenu'; -import SkeletonLoader from '../utils/loader'; +import SkeletonLoader from '../components/SkeletonLoader'; import { Doc, DocumentsProps } from '../models/misc'; import { getDocs } from '../preferences/preferenceApi'; import { setSourceDocs } from '../preferences/preferenceSlice'; diff --git a/frontend/src/settings/Logs.tsx b/frontend/src/settings/Logs.tsx index 1e2bd953..1e248d46 100644 --- a/frontend/src/settings/Logs.tsx +++ b/frontend/src/settings/Logs.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import userService from '../api/services/userService'; import ChevronRight from '../assets/chevron-right.svg'; import Dropdown from '../components/Dropdown'; -import SkeletonLoader from '../utils/loader'; +import SkeletonLoader from '../components/SkeletonLoader'; import { APIKeyData, LogData } from './types'; import CoppyButton from '../components/CopyButton'; diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index 9012059b..37a1fc0c 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -4,6 +4,10 @@ import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import userService from '../api/services/userService'; +import Exit from '../assets/exit.svg'; +import ArrowLeft from '../assets/arrow-left.svg'; +import FileUpload from '../assets/file_upload.svg'; +import WebsiteCollect from '../assets/website_collect.svg'; import Dropdown from '../components/Dropdown'; import Input from '../components/Input'; import { ActiveState, Doc } from '../models/misc'; @@ -17,9 +21,11 @@ import { function Upload({ modalState, setModalState, + isOnboarding, }: { modalState: ActiveState; setModalState: (state: ActiveState) => void; + isOnboarding: boolean; }) { const [docName, setDocName] = useState(''); const [urlName, setUrlName] = useState(''); @@ -32,7 +38,7 @@ function Upload({ search_queries: [''], number_posts: 10, }); - const [activeTab, setActiveTab] = useState('file'); + const [activeTab, setActiveTab] = useState(null); const [files, setfiles] = useState([]); const [progress, setProgress] = useState<{ type: 'UPLOAD' | 'TRAINING'; @@ -66,18 +72,24 @@ function Upload({ function ProgressBar({ progressPercent }: { progressPercent: number }) { return ( -
-
+
+
+
- {progressPercent >= 5 && `${progressPercent}%`} + className={`absolute inset-0 rounded-full ${progressPercent === 100 ? 'shadow-xl shadow-lime-300/50 dark:shadow-lime-300/50 bg-gradient-to-r from-white to-gray-400 dark:bg-gradient-to-br dark:from-gray-500 dark:to-gray-300' : 'shadow-[0_2px_0_#FF3D00_inset] dark:shadow-[0_2px_0_#FF3D00_inset]'}`} + style={{ + animation: `${progressPercent === 100 ? 'none' : 'rotate 2s linear infinite'}`, + }} + >
+
+ {progressPercent}%
+
); @@ -87,20 +99,47 @@ function Upload({ title, isCancellable = false, isFailed = false, + isTraining = false, }: { title: string; isCancellable?: boolean; isFailed?: boolean; + isTraining?: boolean; }) { return (
-

{title}...

+

+ {isTraining && + (progress?.percentage === 100 ? 'Training completed' : title)} + {!isTraining && title} +

This may take several minutes

Over the token limit, please consider uploading smaller document

{/*

{progress?.percentage || 0}%

*/} - + + {isTraining && + (progress?.percentage === 100 ? ( + + ) : ( + + ))}
); } @@ -191,6 +230,7 @@ function Upload({ title="Training is in progress" isCancellable={progress?.percentage === 100} isFailed={progress?.failed === true} + isTraining={true} > ); } @@ -305,32 +345,39 @@ function Upload({ view = ; } else { view = ( - <> -

+

+

{t('modals.uploadDoc.label')}

-
- - -
+ {!activeTab && ( +
+

+ {t('modals.uploadDoc.select')} +

+
+ + +
+
+ )} {activeTab === 'file' && ( <> @@ -519,43 +566,48 @@ function Upload({ )} )} - -
- {activeTab === 'file' ? ( + {activeTab && ( +
+ {activeTab === 'file' ? ( + + ) : ( + + )} - ) : ( - - )} - -
- +
+ )} +
); } @@ -563,9 +615,22 @@ function Upload({
-
+
+ {!isOnboarding && !progress && ( + + )} {view}
diff --git a/lexeu-competition.md b/lexeu-competition.md index 955b3f30..1077de29 100644 --- a/lexeu-competition.md +++ b/lexeu-competition.md @@ -22,8 +22,8 @@ Participants are required to analyze a given test contract by scraping EU law da ### Steps to Participate: 1. **Download Test Contract:** You can download it via this [link](https://docs.google.com/document/d/198d7gFJbVWttkIS9ZRUs_PTKIjhsOUeR/edit?usp=sharing&ouid=107667025862106683614&rtpof=true&sd=true). -2. **Ingest EU Law Data:** Gathe and store data in any format, its available [here](https://eur-lex.europa.eu/browse/directories/legislation.html?displayProfile=lastConsDocProfile&classification=in-force). -3. **Optimized Data Retrieval:** Implement methods to retrieve only small, relevant portions of the law data for efficient analysis of the test contract. Try to create a custom retriever and parser +2. **Ingest EU Law Data:** Gather and store data in any format, it's available [here](https://eur-lex.europa.eu/browse/directories/legislation.html?displayProfile=lastConsDocProfile&classification=in-force). +3. **Optimized Data Retrieval:** Implement methods to retrieve only small, relevant portions of the law data for efficient analysis of the test contract. Try to create a custom retriever and a parser. 4. **Analyze the Contract:** Use your optimized retrieval method to analyze the test contract against the EU law data. 5. **Submission Criteria:** Your solution will be judged based on: - Amount of corrections/inconsistencies found @@ -41,7 +41,7 @@ Participants are required to analyze a given test contract by scraping EU law da - **Documentation:** Refer to our [Documentation](https://docs.docsgpt.cloud/) for guidance. - **Discord Support:** Join our [Discord](https://discord.gg/n5BX8dh8rU) server for support and discussions related to the competition. - Try looking at existing [retrievers](https://github.com/arc53/DocsGPT/tree/main/application/retriever) and maybe creating a custom one -- Try looking at [worker.py](https://github.com/arc53/DocsGPT/blob/main/application/worker.py) which ingests data and creating a custom one for EU law ingestion +- Try looking at [worker.py](https://github.com/arc53/DocsGPT/blob/main/application/worker.py) which ingests data and creating a custom one for ingesting EU law ## 👥 Community and Support: diff --git a/setup.sh b/setup.sh index 988841da..7980461b 100755 --- a/setup.sh +++ b/setup.sh @@ -9,6 +9,39 @@ prompt_user() { read -p "Enter your choice (1, 2 or 3): " choice } +check_and_start_docker() { + # Check if Docker is running + if ! docker info > /dev/null 2>&1; then + echo "Docker is not running. Starting Docker..." + + # Check the operating system + case "$(uname -s)" in + Darwin) + open -a Docker + ;; + Linux) + sudo systemctl start docker + ;; + *) + echo "Unsupported platform. Please start Docker manually." + exit 1 + ;; + esac + + # Wait for Docker to be fully operational with animated dots + echo -n "Waiting for Docker to start" + while ! docker system info > /dev/null 2>&1; do + for i in {1..3}; do + echo -n "." + sleep 1 + done + echo -ne "\rWaiting for Docker to start " # Reset to overwrite previous dots + done + + echo -e "\nDocker has started!" + fi +} + # Function to handle the choice to download the model locally download_locally() { echo "LLM_NAME=llama.cpp" > .env @@ -30,6 +63,9 @@ download_locally() { echo "Model already exists." fi + # Call the function to check and start Docker if needed + check_and_start_docker + docker-compose -f docker-compose-local.yaml build && docker-compose -f docker-compose-local.yaml up -d #python -m venv venv #source venv/bin/activate @@ -59,10 +95,11 @@ use_openai() { echo "VITE_API_STREAMING=true" >> .env echo "The .env file has been created with API_KEY set to your provided key." + # Call the function to check and start Docker if needed + check_and_start_docker + docker-compose build && docker-compose up -d - - echo "The application will run on http://localhost:5173" echo "You can stop the application by running the following command:" echo "docker-compose down" @@ -73,6 +110,9 @@ use_docsgpt() { echo "VITE_API_STREAMING=true" >> .env echo "The .env file has been created with API_KEY set to your provided key." + # Call the function to check and start Docker if needed + check_and_start_docker + docker-compose build && docker-compose up -d echo "The application will run on http://localhost:5173" diff --git a/tests/llm/test_anthropic.py b/tests/llm/test_anthropic.py index ee4ba15f..689013c0 100644 --- a/tests/llm/test_anthropic.py +++ b/tests/llm/test_anthropic.py @@ -22,17 +22,23 @@ class TestAnthropicLLM(unittest.TestCase): mock_response = Mock() mock_response.completion = "test completion" - with patch.object(self.llm.anthropic.completions, "create", return_value=mock_response) as mock_create: - response = self.llm.gen("test_model", messages) - self.assertEqual(response, "test completion") + with patch("application.cache.get_redis_instance") as mock_make_redis: + mock_redis_instance = mock_make_redis.return_value + mock_redis_instance.get.return_value = None + mock_redis_instance.set = Mock() - prompt_expected = "### Context \n context \n ### Question \n question" - mock_create.assert_called_with( - model="test_model", - max_tokens_to_sample=300, - stream=False, - prompt=f"{self.llm.HUMAN_PROMPT} {prompt_expected}{self.llm.AI_PROMPT}" - ) + with patch.object(self.llm.anthropic.completions, "create", return_value=mock_response) as mock_create: + response = self.llm.gen("test_model", messages) + self.assertEqual(response, "test completion") + + prompt_expected = "### Context \n context \n ### Question \n question" + mock_create.assert_called_with( + model="test_model", + max_tokens_to_sample=300, + stream=False, + prompt=f"{self.llm.HUMAN_PROMPT} {prompt_expected}{self.llm.AI_PROMPT}" + ) + mock_redis_instance.set.assert_called_once() def test_gen_stream(self): messages = [ @@ -41,17 +47,23 @@ class TestAnthropicLLM(unittest.TestCase): ] mock_responses = [Mock(completion="response_1"), Mock(completion="response_2")] - with patch.object(self.llm.anthropic.completions, "create", return_value=iter(mock_responses)) as mock_create: - responses = list(self.llm.gen_stream("test_model", messages)) - self.assertListEqual(responses, ["response_1", "response_2"]) + with patch("application.cache.get_redis_instance") as mock_make_redis: + mock_redis_instance = mock_make_redis.return_value + mock_redis_instance.get.return_value = None + mock_redis_instance.set = Mock() - prompt_expected = "### Context \n context \n ### Question \n question" - mock_create.assert_called_with( - model="test_model", - prompt=f"{self.llm.HUMAN_PROMPT} {prompt_expected}{self.llm.AI_PROMPT}", - max_tokens_to_sample=300, - stream=True - ) + with patch.object(self.llm.anthropic.completions, "create", return_value=iter(mock_responses)) as mock_create: + responses = list(self.llm.gen_stream("test_model", messages)) + self.assertListEqual(responses, ["response_1", "response_2"]) + + prompt_expected = "### Context \n context \n ### Question \n question" + mock_create.assert_called_with( + model="test_model", + prompt=f"{self.llm.HUMAN_PROMPT} {prompt_expected}{self.llm.AI_PROMPT}", + max_tokens_to_sample=300, + stream=True + ) + mock_redis_instance.set.assert_called_once() if __name__ == "__main__": unittest.main() diff --git a/tests/llm/test_sagemaker.py b/tests/llm/test_sagemaker.py index 0602f597..d659d498 100644 --- a/tests/llm/test_sagemaker.py +++ b/tests/llm/test_sagemaker.py @@ -52,28 +52,38 @@ class TestSagemakerAPILLM(unittest.TestCase): self.response['Body'].read.return_value.decode.return_value = json.dumps(self.result) def test_gen(self): - with patch.object(self.sagemaker.runtime, 'invoke_endpoint', - return_value=self.response) as mock_invoke_endpoint: - output = self.sagemaker.gen(None, self.messages) - mock_invoke_endpoint.assert_called_once_with( - EndpointName=self.sagemaker.endpoint, - ContentType='application/json', - Body=self.body_bytes - ) - self.assertEqual(output, - self.result[0]['generated_text'][len(self.prompt):]) + with patch('application.cache.get_redis_instance') as mock_make_redis: + mock_redis_instance = mock_make_redis.return_value + mock_redis_instance.get.return_value = None + + with patch.object(self.sagemaker.runtime, 'invoke_endpoint', + return_value=self.response) as mock_invoke_endpoint: + output = self.sagemaker.gen(None, self.messages) + mock_invoke_endpoint.assert_called_once_with( + EndpointName=self.sagemaker.endpoint, + ContentType='application/json', + Body=self.body_bytes + ) + self.assertEqual(output, + self.result[0]['generated_text'][len(self.prompt):]) + mock_make_redis.assert_called_once() + mock_redis_instance.set.assert_called_once() def test_gen_stream(self): - with patch.object(self.sagemaker.runtime, 'invoke_endpoint_with_response_stream', - return_value=self.response) as mock_invoke_endpoint: - output = list(self.sagemaker.gen_stream(None, self.messages)) - mock_invoke_endpoint.assert_called_once_with( - EndpointName=self.sagemaker.endpoint, - ContentType='application/json', - Body=self.body_bytes_stream - ) - self.assertEqual(output, []) - + with patch('application.cache.get_redis_instance') as mock_make_redis: + mock_redis_instance = mock_make_redis.return_value + mock_redis_instance.get.return_value = None + + with patch.object(self.sagemaker.runtime, 'invoke_endpoint_with_response_stream', + return_value=self.response) as mock_invoke_endpoint: + output = list(self.sagemaker.gen_stream(None, self.messages)) + mock_invoke_endpoint.assert_called_once_with( + EndpointName=self.sagemaker.endpoint, + ContentType='application/json', + Body=self.body_bytes_stream + ) + self.assertEqual(output, []) + mock_redis_instance.set.assert_called_once() class TestLineIterator(unittest.TestCase): def setUp(self): diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 00000000..4270a181 --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,131 @@ +import unittest +import json +from unittest.mock import patch, MagicMock +from application.cache import gen_cache_key, stream_cache, gen_cache +from application.utils import get_hash + + +# Test for gen_cache_key function +def test_make_gen_cache_key(): + messages = [ + {'role': 'user', 'content': 'test_user_message'}, + {'role': 'system', 'content': 'test_system_message'}, + ] + model = "test_docgpt" + + # Manually calculate the expected hash + expected_combined = f"{model}_{json.dumps(messages, sort_keys=True)}" + expected_hash = get_hash(expected_combined) + cache_key = gen_cache_key(*messages, model=model) + + assert cache_key == expected_hash + +def test_gen_cache_key_invalid_message_format(): + # Test when messages is not a list + with unittest.TestCase.assertRaises(unittest.TestCase, ValueError) as context: + gen_cache_key("This is not a list", model="docgpt") + assert str(context.exception) == "All messages must be dictionaries." + +# Test for gen_cache decorator +@patch('application.cache.get_redis_instance') # Mock the Redis client +def test_gen_cache_hit(mock_make_redis): + # Arrange + mock_redis_instance = MagicMock() + mock_make_redis.return_value = mock_redis_instance + mock_redis_instance.get.return_value = b"cached_result" # Simulate a cache hit + + @gen_cache + def mock_function(self, model, messages): + return "new_result" + + messages = [{'role': 'user', 'content': 'test_user_message'}] + model = "test_docgpt" + + # Act + result = mock_function(None, model, messages) + + # Assert + assert result == "cached_result" # Should return cached result + mock_redis_instance.get.assert_called_once() # Ensure Redis get was called + mock_redis_instance.set.assert_not_called() # Ensure the function result is not cached again + + +@patch('application.cache.get_redis_instance') # Mock the Redis client +def test_gen_cache_miss(mock_make_redis): + # Arrange + mock_redis_instance = MagicMock() + mock_make_redis.return_value = mock_redis_instance + mock_redis_instance.get.return_value = None # Simulate a cache miss + + @gen_cache + def mock_function(self, model, messages): + return "new_result" + + messages = [ + {'role': 'user', 'content': 'test_user_message'}, + {'role': 'system', 'content': 'test_system_message'}, + ] + model = "test_docgpt" + # Act + result = mock_function(None, model, messages) + + # Assert + assert result == "new_result" + mock_redis_instance.get.assert_called_once() + +@patch('application.cache.get_redis_instance') +def test_stream_cache_hit(mock_make_redis): + # Arrange + mock_redis_instance = MagicMock() + mock_make_redis.return_value = mock_redis_instance + + cached_chunk = json.dumps(["chunk1", "chunk2"]).encode('utf-8') + mock_redis_instance.get.return_value = cached_chunk + + @stream_cache + def mock_function(self, model, messages, stream): + yield "new_chunk" + + messages = [{'role': 'user', 'content': 'test_user_message'}] + model = "test_docgpt" + + # Act + result = list(mock_function(None, model, messages, stream=True)) + + # Assert + assert result == ["chunk1", "chunk2"] # Should return cached chunks + mock_redis_instance.get.assert_called_once() + mock_redis_instance.set.assert_not_called() + + +@patch('application.cache.get_redis_instance') +def test_stream_cache_miss(mock_make_redis): + # Arrange + mock_redis_instance = MagicMock() + mock_make_redis.return_value = mock_redis_instance + mock_redis_instance.get.return_value = None # Simulate a cache miss + + @stream_cache + def mock_function(self, model, messages, stream): + yield "new_chunk" + + messages = [ + {'role': 'user', 'content': 'This is the context'}, + {'role': 'system', 'content': 'Some other message'}, + {'role': 'user', 'content': 'What is the answer?'} + ] + model = "test_docgpt" + + # Act + result = list(mock_function(None, model, messages, stream=True)) + + # Assert + assert result == ["new_chunk"] + mock_redis_instance.get.assert_called_once() + mock_redis_instance.set.assert_called_once() + + + + + +