diff --git a/.github/ISSUE_TEMPLATE/01_bug.yml b/.github/ISSUE_TEMPLATE/01_bug.yml new file mode 100644 index 000000000..02ff8a442 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_bug.yml @@ -0,0 +1,44 @@ +name: 🐛 Bug Report +description: File a bug report for AnythingLLM +title: "[BUG]: " +labels: [possible bug] +body: + - type: markdown + attributes: + value: | + Use this template to file a bug report for AnythingLLM. Please be as descriptive as possible to allow everyone to replicate and solve your issue. + + Want help contributing a PR? Use our repo chatbot by OnboardAI! https://learnthisrepo.com/anythingllm + + - type: dropdown + id: runtime + attributes: + label: How are you running AnythingLLM? + description: AnythingLLM can be run in many environments, pick the one that best represents where you encounter the bug. + options: + - Docker (local) + - Docker (remote machine) + - Local development + - AnythingLLM desktop app + - Not listed + default: 0 + validations: + required: true + + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Are there known steps to reproduce? + description: | + Let us know how to reproduce the bug and we may be able to fix it more + quickly. This is not required, but it is helpful. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/02_feature.yml b/.github/ISSUE_TEMPLATE/02_feature.yml new file mode 100644 index 000000000..ab2be3abd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_feature.yml @@ -0,0 +1,22 @@ +name: ✨ New Feature suggestion +description: Suggest a new feature for AnythingLLM! +title: "[FEAT]: " +labels: [enhancement, feature request] +body: + - type: markdown + attributes: + value: | + Share a new idea for a feature or improvement. Be sure to search existing + issues first to avoid duplicates. + + Want help contributing a PR? Use our repo chatbot by OnboardAI! https://learnthisrepo.com/anythingllm + + + - type: textarea + id: description + attributes: + label: What would you like to see? + description: | + Describe the feature and why it would be useful to your use-case as well as others. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/03_documentation.yml b/.github/ISSUE_TEMPLATE/03_documentation.yml new file mode 100644 index 000000000..55800856c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_documentation.yml @@ -0,0 +1,13 @@ +name: 📚 Documentation improvement +title: "[DOCS]: " +description: Report an issue or problem with the documentation. +labels: [documentation] + +body: + - type: textarea + id: description + attributes: + label: Description + description: Describe the issue with the documentation that is giving you trouble or causing confusion. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..d5485e65d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: 🧑🤝🧑 Community Discord + url: https://discord.gg/6UyHPeGZAC + about: Interact with the Mintplex Labs community here by asking for help, discussing and more! diff --git a/.github/workflows/build-and-push-image.yaml b/.github/workflows/build-and-push-image.yaml index b8ac6348f..12b274b75 100644 --- a/.github/workflows/build-and-push-image.yaml +++ b/.github/workflows/build-and-push-image.yaml @@ -14,11 +14,12 @@ on: push: branches: ['master'] # master branch only. Do not modify. paths-ignore: - - '*.md' + - '**.md' - 'cloud-deployments/*' - - 'images/*' - - '.vscode/*' + - 'images/**/*' + - '.vscode/**/*' - '**/.env.example' + - '.github/ISSUE_TEMPLATE/**/*' jobs: push_multi_platform_to_registries: @@ -31,10 +32,18 @@ jobs: - name: Check out the repo uses: actions/checkout@v4 - - name: Parse repository name to lowercase + - name: Check if DockerHub build needed shell: bash - run: echo "repo=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT - id: lowercase_repo + run: | + # Check if the secret for USERNAME is set (don't even check for the password) + if [[ -z "${{ secrets.DOCKER_USERNAME }}" ]]; then + echo "DockerHub build not needed" + echo "enabled=false" >> $GITHUB_OUTPUT + else + echo "DockerHub build needed" + echo "enabled=true" >> $GITHUB_OUTPUT + fi + id: dockerhub - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -44,6 +53,8 @@ jobs: - name: Log in to Docker Hub uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a + # Only login to the Docker Hub if the repo is mintplex/anythingllm, to allow for forks to build on GHCR + if: steps.dockerhub.outputs.enabled == 'true' with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -60,9 +71,15 @@ jobs: uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 with: images: | - mintplexlabs/anythingllm + ${{ steps.dockerhub.outputs.enabled == 'true' && 'mintplexlabs/anythingllm' || '' }} ghcr.io/${{ github.repository }} - + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + + - name: Build and push multi-platform Docker image uses: docker/build-push-action@v5 with: @@ -70,8 +87,7 @@ jobs: file: ./docker/Dockerfile push: true platforms: linux/amd64,linux/arm64 - tags: | - ${{ steps.meta.outputs.tags }} - ${{ github.ref_name == 'master' && 'mintplexlabs/anythingllm:latest' || '' }} - ${{ github.ref_name == 'master' && format('ghcr.io/{0}:{1}', steps.lowercase_repo.outputs.repo, 'latest') || '' }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/build-and-push-render-deployment-image.yaml b/.github/workflows/build-and-push-render-deployment-image.yaml index 7e6a343c1..7f40dc0be 100644 --- a/.github/workflows/build-and-push-render-deployment-image.yaml +++ b/.github/workflows/build-and-push-render-deployment-image.yaml @@ -8,11 +8,13 @@ on: push: branches: ['render'] paths-ignore: - - 'render.yaml' - - '*.md' + - '**.md' - 'cloud-deployments/*' - - 'images/*' - - '.vscode/*' + - 'images/**/*' + - '.vscode/**/*' + - '**/.env.example' + - '.github/ISSUE_TEMPLATE/**/*' + - 'render.yaml' jobs: push_to_registries: diff --git a/.vscode/settings.json b/.vscode/settings.json index 82165a178..ab66c194b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,10 +2,13 @@ "cSpell.words": [ "Dockerized", "Langchain", + "Milvus", "Ollama", "openai", "Qdrant", - "Weaviate" + "vectordbs", + "Weaviate", + "Zilliz" ], "eslint.experimental.useFlatConfig": true } \ No newline at end of file diff --git a/README.md b/README.md index 62d58d870..c3eb429c0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ +
@@ -38,13 +39,14 @@ A full-stack application that enables you to turn any document, resource, or pie - ### Product Overview + AnythingLLM is a full-stack application where you can use commercial off-the-shelf LLMs or popular open source LLMs and vectorDB solutions to build a private ChatGPT with no compromises that you can run locally as well as host remotely and be able to chat intelligently with any documents you provide it. AnythingLLM divides your documents into objects called `workspaces`. A Workspace functions a lot like a thread, but with the addition of containerization of your documents. Workspaces can share documents, but they do not talk to each other so you can keep your context for each workspace clean. Some cool features of AnythingLLM + - **Multi-user instance support and permissioning** - Multiple document type support (PDF, TXT, DOCX, etc) - Manage documents in your vector database from a simple UI @@ -57,7 +59,9 @@ Some cool features of AnythingLLM - Full Developer API for custom integrations! ### Supported LLMs, Embedders, and Vector Databases + **Supported LLMs:** + - [Any open-source llama.cpp compatible model](/server/storage/models/README.md#text-generation-llm-selection) - [OpenAI](https://openai.com) - [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) @@ -66,8 +70,11 @@ Some cool features of AnythingLLM - [Ollama (chat models)](https://ollama.ai/) - [LM Studio (all models)](https://lmstudio.ai) - [LocalAi (all models)](https://localai.io/) +- [Together AI (chat models)](https://www.together.ai/) +- [Mistral](https://mistral.ai/) **Supported Embedding models:** + - [AnythingLLM Native Embedder](/server/storage/models/README.md) (default) - [OpenAI](https://openai.com) - [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) @@ -75,42 +82,45 @@ Some cool features of AnythingLLM - [LocalAi (all)](https://localai.io/) **Supported Vector Databases:** + - [LanceDB](https://github.com/lancedb/lancedb) (default) - [Pinecone](https://pinecone.io) - [Chroma](https://trychroma.com) - [Weaviate](https://weaviate.io) - [QDrant](https://qdrant.tech) - +- [Milvus](https://milvus.io) +- [Zilliz](https://zilliz.com) ### Technical Overview + This monorepo consists of three main sections: + - `frontend`: A viteJS + React frontend that you can run to easily create and manage all your content the LLM can use. - `server`: A NodeJS express server to handle all the interactions and do all the vectorDB management and LLM interactions. - `docker`: Docker instructions and build process + information for building from source. - `collector`: NodeJS express server that process and parses documents from the UI. ## 🛳 Self Hosting -Mintplex Labs & the community maintain a number of deployment methods, scripts, and templates that you can use to run AnythingLLM locally. Refer to the table below to read how to deploy on your preferred environment or to automatically deploy. -| Docker | AWS | GCP | Digital Ocean | Render.com | -|----------------------------------------|----:|-----|---------------|------------| -| [![Deploy on Docker][docker-btn]][docker-deploy] | [![Deploy on AWS][aws-btn]][aws-deploy] | [![Deploy on GCP][gcp-btn]][gcp-deploy] | [![Deploy on DigitalOcean][do-btn]][aws-deploy] | [![Deploy on Render.com][render-btn]][render-deploy] | +Mintplex Labs & the community maintain a number of deployment methods, scripts, and templates that you can use to run AnythingLLM locally. Refer to the table below to read how to deploy on your preferred environment or to automatically deploy. +| Docker | AWS | GCP | Digital Ocean | Render.com | +|----------------------------------------|----:|-----|---------------|------------| +| [![Deploy on Docker][docker-btn]][docker-deploy] | [![Deploy on AWS][aws-btn]][aws-deploy] | [![Deploy on GCP][gcp-btn]][gcp-deploy] | [![Deploy on DigitalOcean][do-btn]][aws-deploy] | [![Deploy on Render.com][render-btn]][render-deploy] | ## How to setup for development + - `yarn setup` To fill in the required `.env` files you'll need in each of the application sections (from root of repo). - Go fill those out before proceeding. Ensure `server/.env.development` is filled or else things won't work right. - `yarn dev:server` To boot the server locally (from root of repo). - `yarn dev:frontend` To boot the frontend locally (from root of repo). - `yarn dev:collector` To then run the document collector (from root of repo). - - - [Learn about documents](./server/storage/documents/DOCUMENTS.md) [Learn about vector caching](./server/storage/vector-cache/VECTOR_CACHE.md) ## Contributing + - create issue - create PR with branch name format of `+ The specific chat model that will be used for this workspace. If + empty, will use the system LLM preference. +
++ The specific chat model that will be used for this workspace. If + empty, will use the system LLM preference. +
++ {" "} +
{workspace?.slug}
@@ -101,13 +102,7 @@ export default function WorkspaceSettings({ workspace }) {Total number of vectors in your vector database.
- {totalVectors !== null ? ( -- {totalVectors} -
- ) : ( -- Referenced {references} times. -
- )} -+ Referenced {references} times. +
+ )}+
Welcome to your new workspace.
+
To get started either{" "}
);
})}
-
{showing && (
` + - hljs.highlight(lang, str, true).value + + `+" ); } catch (__) {} } return ( - `++++${lang || ""}
++ + +Copy code
+` + + hljs.highlight(code, { language: lang, ignoreIllegals: true }).value + "Copy code ` + - HTMLEncode(str) + + `+" ); }, }); -window.copySnippet = function (uuid = "") { - const target = document.getElementById(`code-${uuid}`); - const markdown = - target.parentElement?.parentElement?.querySelector( - "pre:first-of-type" - )?.innerText; - if (!markdown) return false; - - window.navigator.clipboard.writeText(markdown); - target.classList.add("text-green-500"); - const originalText = target.innerHTML; - target.innerText = "Copied!"; - target.setAttribute("disabled", true); - - setTimeout(() => { - target.classList.remove("text-green-500"); - target.innerHTML = originalText; - target.removeAttribute("disabled"); - }, 5000); -}; - export default function renderMarkdown(text = "") { return markdown.render(text); } diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 11b8da976..2fde1ee00 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -7,3 +7,8 @@ export const COMPLETE_QUESTIONNAIRE = "anythingllm_completed_questionnaire"; export const USER_BACKGROUND_COLOR = "bg-historical-msg-user"; export const AI_BACKGROUND_COLOR = "bg-historical-msg-system"; + +export function fullApiUrl() { + if (API_BASE !== "/api") return API_BASE; + return `${window.location.origin}/api`; +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index e7b223df9..fa1e71331 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -365,6 +365,26 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.53.0.tgz#bea56f2ed2b5baea164348ff4d5a879f6f81f20d" integrity sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w== +"@floating-ui/core@^1.5.3": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.3.tgz#b6aa0827708d70971c8679a16cf680a515b8a52a" + integrity sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q== + dependencies: + "@floating-ui/utils" "^0.2.0" + +"@floating-ui/dom@^1.0.0": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.4.tgz#28df1e1cb373884224a463235c218dcbd81a16bb" + integrity sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ== + dependencies: + "@floating-ui/core" "^1.5.3" + "@floating-ui/utils" "^0.2.0" + +"@floating-ui/utils@^0.2.0": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" + integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== + "@humanwhocodes/config-array@^0.11.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -846,6 +866,11 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +classnames@^2.3.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -1021,6 +1046,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dompurify@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.8.tgz#e0021ab1b09184bc8af7e35c7dd9063f43a8a437" + integrity sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ== + electron-to-chromium@^1.4.535: version "1.4.576" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz#0c6940fdc0d60f7e34bd742b29d8fa847c9294d1" @@ -2538,6 +2568,14 @@ react-toastify@^9.1.3: dependencies: clsx "^1.1.1" +react-tooltip@^5.25.2: + version "5.25.2" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.25.2.tgz#efb51845ec2e863045812ad1dc1927573922d629" + integrity sha512-MwZ3S9xcHpojZaKqjr5mTs0yp/YBPpKFcayY7MaaIIBr2QskkeeyelpY2YdGLxIMyEj4sxl0rGoK6dQIKvNLlw== + dependencies: + "@floating-ui/dom" "^1.0.0" + classnames "^2.3.0" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" diff --git a/pull_request_template.md b/pull_request_template.md new file mode 100644 index 000000000..1167880b1 --- /dev/null +++ b/pull_request_template.md @@ -0,0 +1,36 @@ + + ### Pull Request Type + + + +- [ ] ✨ feat +- [ ] 🐛 fix +- [ ] ♻️ refactor +- [ ] 💄 style +- [ ] 🔨 chore +- [ ] 📝 docs + +### Relevant Issues + + + +resolves #xxx + + +### What is in this change? + +Describe the changes in this PR that are impactful to the repo. + + +### Additional Information + +Add any other context about the Pull Request here that was not captured above. + +### Developer Validations + + + +- [ ] I ran `yarn lint` from the root of the repo & committed changes +- [ ] Relevant documentation has been updated +- [ ] I have tested my code functionality +- [ ] Docker build succeeds locally diff --git a/server/.env.example b/server/.env.example index 5b159a03d..23e20bb13 100644 --- a/server/.env.example +++ b/server/.env.example @@ -37,6 +37,14 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea # OLLAMA_MODEL_PREF='llama2' # OLLAMA_MODEL_TOKEN_LIMIT=4096 +# LLM_PROVIDER='togetherai' +# TOGETHER_AI_API_KEY='my-together-ai-key' +# TOGETHER_AI_MODEL_PREF='mistralai/Mixtral-8x7B-Instruct-v0.1' + +# LLM_PROVIDER='mistral' +# MISTRAL_API_KEY='example-mistral-ai-api-key' +# MISTRAL_MODEL_PREF='mistral-tiny' + ########################################### ######## Embedding API SElECTION ########## ########################################### @@ -82,6 +90,16 @@ VECTOR_DB="lancedb" # QDRANT_ENDPOINT="http://localhost:6333" # QDRANT_API_KEY= +# Enable all below if you are using vector database: Milvus. +# VECTOR_DB="milvus" +# MILVUS_ADDRESS="http://localhost:19530" +# MILVUS_USERNAME= +# MILVUS_PASSWORD= + +# Enable all below if you are using vector database: Zilliz Cloud. +# VECTOR_DB="zilliz" +# ZILLIZ_ENDPOINT="https://sample.api.gcp-us-west1.zillizcloud.com" +# ZILLIZ_API_TOKEN=api-token-here # CLOUD DEPLOYMENT VARIRABLES ONLY # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting. diff --git a/server/.gitignore b/server/.gitignore index be4af591d..0913f9663 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -3,6 +3,7 @@ storage/assets/* !storage/assets/anything-llm.png storage/documents/* +storage/tmp/* storage/vector-cache/*.json storage/exports storage/imports diff --git a/server/endpoints/api/document/index.js b/server/endpoints/api/document/index.js index a813e2df6..817043526 100644 --- a/server/endpoints/api/document/index.js +++ b/server/endpoints/api/document/index.js @@ -5,8 +5,13 @@ const { checkProcessorAlive, acceptedFileTypes, processDocument, + processLink, } = require("../../../utils/files/documentProcessor"); -const { viewLocalFiles } = require("../../../utils/files"); +const { + viewLocalFiles, + findDocumentInDocuments, +} = require("../../../utils/files"); +const { reqBody } = require("../../../utils/http"); const { handleUploads } = setupMulter(); function apiDocumentEndpoints(app) { @@ -20,7 +25,6 @@ function apiDocumentEndpoints(app) { /* #swagger.tags = ['Documents'] #swagger.description = 'Upload a new file to AnythingLLM to be parsed and prepared for embedding.' - #swagger.requestBody = { description: 'File to be uploaded.', required: true, @@ -47,6 +51,21 @@ function apiDocumentEndpoints(app) { example: { success: true, error: null, + documents: [ + { + "location": "custom-documents/anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json", + "name": "anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json", + "url": "file:///Users/tim/Documents/anything-llm/collector/hotdir/anythingllm.txt", + "title": "anythingllm.txt", + "docAuthor": "Unknown", + "description": "Unknown", + "docSource": "a text file uploaded by the user.", + "chunkSource": "anythingllm.txt", + "published": "1/16/2024, 3:07:00 PM", + "wordCount": 93, + "token_count_estimate": 115, + } + ] } } } @@ -72,16 +91,113 @@ function apiDocumentEndpoints(app) { .end(); } - const { success, reason } = await processDocument(originalname); + const { success, reason, documents } = + await processDocument(originalname); if (!success) { - response.status(500).json({ success: false, error: reason }).end(); + response + .status(500) + .json({ success: false, error: reason, documents }) + .end(); + return; } console.log( `Document ${originalname} uploaded processed and successfully. It is now available in documents.` ); await Telemetry.sendTelemetry("document_uploaded"); - response.status(200).json({ success: true, error: null }); + response.status(200).json({ success: true, error: null, documents }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.post( + "/v1/document/upload-link", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Documents'] + #swagger.description = 'Upload a valid URL for AnythingLLM to scrape and prepare for embedding.' + #swagger.requestBody = { + description: 'Link of web address to be scraped.', + required: true, + type: 'file', + content: { + "application/json": { + schema: { + type: 'object', + example: { + "link": "https://useanything.com" + } + } + } + } + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + success: true, + error: null, + documents: [ + { + "id": "c530dbe6-bff1-4b9e-b87f-710d539d20bc", + "url": "file://useanything_com.html", + "title": "useanything_com.html", + "docAuthor": "no author found", + "description": "No description found.", + "docSource": "URL link uploaded by the user.", + "chunkSource": "https:useanything.com.html", + "published": "1/16/2024, 3:46:33 PM", + "wordCount": 252, + "pageContent": "AnythingLLM is the best....", + "token_count_estimate": 447, + "location": "custom-documents/url-useanything_com-c530dbe6-bff1-4b9e-b87f-710d539d20bc.json" + } + ] + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { link } = reqBody(request); + const processingOnline = await checkProcessorAlive(); + + if (!processingOnline) { + response + .status(500) + .json({ + success: false, + error: `Document processing API is not online. Link ${link} will not be processed automatically.`, + }) + .end(); + } + + const { success, reason, documents } = await processLink(link); + if (!success) { + response + .status(500) + .json({ success: false, error: reason, documents }) + .end(); + return; + } + + console.log( + `Link ${link} uploaded processed and successfully. It is now available in documents.` + ); + await Telemetry.sendTelemetry("document_uploaded"); + response.status(200).json({ success: true, error: null, documents }); } catch (e) { console.log(e.message, e); response.sendStatus(500).end(); @@ -133,6 +249,61 @@ function apiDocumentEndpoints(app) { } }); + app.get("/v1/document/:docName", [validApiKey], async (request, response) => { + /* + #swagger.tags = ['Documents'] + #swagger.description = 'Get a single document by its unique AnythingLLM document name' + #swagger.parameters['docName'] = { + in: 'path', + description: 'Unique document name to find (name in /documents)', + required: true, + type: 'string' + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + "localFiles": { + "name": "documents", + "type": "folder", + items: [ + { + "name": "my-stored-document.txt-uuid1234.json", + "type": "file", + "id": "bb07c334-4dab-4419-9462-9d00065a49a1", + "url": "file://my-stored-document.txt", + "title": "my-stored-document.txt", + "cached": false + }, + ] + } + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { docName } = request.params; + const document = await findDocumentInDocuments(docName); + if (!document) { + response.sendStatus(404).end(); + return; + } + response.status(200).json({ document }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + app.get( "/v1/document/accepted-file-types", [validApiKey], diff --git a/server/endpoints/api/system/index.js b/server/endpoints/api/system/index.js index 3548c3068..b18019b14 100644 --- a/server/endpoints/api/system/index.js +++ b/server/endpoints/api/system/index.js @@ -139,7 +139,7 @@ function apiSystemEndpoints(app) { */ try { const body = reqBody(request); - const { newValues, error } = updateENV(body); + const { newValues, error } = await updateENV(body); if (process.env.NODE_ENV === "production") await dumpENV(); response.status(200).json({ newValues, error }); } catch (e) { diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js index 032fe41c3..c1642ce4a 100644 --- a/server/endpoints/api/workspace/index.js +++ b/server/endpoints/api/workspace/index.js @@ -11,6 +11,11 @@ const { const { getVectorDbClass } = require("../../../utils/helpers"); const { multiUserMode, reqBody } = require("../../../utils/http"); const { validApiKey } = require("../../../utils/middleware/validApiKey"); +const { + streamChatWithWorkspace, + writeResponseChunk, + VALID_CHAT_MODE, +} = require("../../../utils/chats/stream"); function apiWorkspaceEndpoints(app) { if (!app) return; @@ -196,10 +201,11 @@ function apiWorkspaceEndpoints(app) { return; } - await WorkspaceChats.delete({ workspaceId: Number(workspace.id) }); - await DocumentVectors.deleteForWorkspace(Number(workspace.id)); - await Document.delete({ workspaceId: Number(workspace.id) }); - await Workspace.delete({ id: Number(workspace.id) }); + const workspaceId = Number(workspace.id); + await WorkspaceChats.delete({ workspaceId: workspaceId }); + await DocumentVectors.deleteForWorkspace(workspaceId); + await Document.delete({ workspaceId: workspaceId }); + await Workspace.delete({ id: workspaceId }); try { await VectorDb["delete-namespace"]({ namespace: slug }); } catch (e) { @@ -375,8 +381,8 @@ function apiWorkspaceEndpoints(app) { content: { "application/json": { example: { - adds: [], - deletes: ["custom-documents/anythingllm-hash.json"] + adds: ["custom-documents/my-pdf.pdf-hash.json"], + deletes: ["custom-documents/anythingllm.txt-hash.json"] } } } @@ -441,7 +447,7 @@ function apiWorkspaceEndpoints(app) { #swagger.tags = ['Workspaces'] #swagger.description = 'Execute a chat with a workspace' #swagger.requestBody = { - description: 'prompt to send to the workspace and the type of conversation (query or chat).', + description: 'Send a prompt to the workspace and the type of conversation (query or chat).++++ + +Copy code
+` + + HTMLEncode(code) + "
Query: Will not use LLM unless there are relevant sources from vectorDB & does not recall chat history.
Chat: Uses LLM general knowledge w/custom embeddings to produce output, uses rolling chat history.', required: true, type: 'object', content: { @@ -482,7 +488,28 @@ function apiWorkspaceEndpoints(app) { const workspace = await Workspace.get({ slug }); if (!workspace) { - response.sendStatus(400).end(); + response.status(400).json({ + id: uuidv4(), + type: "abort", + textResponse: null, + sources: [], + close: true, + error: `Workspace ${slug} is not a valid workspace.`, + }); + return; + } + + if (!message?.length || !VALID_CHAT_MODE.includes(mode)) { + response.status(400).json({ + id: uuidv4(), + type: "abort", + textResponse: null, + sources: [], + close: true, + error: !message?.length + ? "message parameter cannot be empty." + : `${mode} is not a valid mode.`, + }); return; } @@ -505,6 +532,126 @@ function apiWorkspaceEndpoints(app) { } } ); + + app.post( + "/v1/workspace/:slug/stream-chat", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Workspaces'] + #swagger.description = 'Execute a streamable chat with a workspace' + #swagger.requestBody = { + description: 'Send a prompt to the workspace and the type of conversation (query or chat).
Query: Will not use LLM unless there are relevant sources from vectorDB & does not recall chat history.
Chat: Uses LLM general knowledge w/custom embeddings to produce output, uses rolling chat history.', + required: true, + type: 'object', + content: { + "application/json": { + example: { + message: "What is AnythingLLM?", + mode: "query | chat" + } + } + } + } + #swagger.responses[200] = { + content: { + "text/event-stream": { + schema: { + type: 'array', + example: [ + { + id: 'uuid-123', + type: "abort | textResponseChunk", + textResponse: "First chunk", + sources: [], + close: false, + error: "null | text string of the failure mode." + }, + { + id: 'uuid-123', + type: "abort | textResponseChunk", + textResponse: "chunk two", + sources: [], + close: false, + error: "null | text string of the failure mode." + }, + { + id: 'uuid-123', + type: "abort | textResponseChunk", + textResponse: "final chunk of LLM output!", + sources: [{title: "anythingllm.txt", chunk: "This is a context chunk used in the answer of the prompt by the LLM. This will only return in the final chunk."}], + close: true, + error: "null | text string of the failure mode." + } + ] + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { slug } = request.params; + const { message, mode = "query" } = reqBody(request); + const workspace = await Workspace.get({ slug }); + + if (!workspace) { + response.status(400).json({ + id: uuidv4(), + type: "abort", + textResponse: null, + sources: [], + close: true, + error: `Workspace ${slug} is not a valid workspace.`, + }); + return; + } + + if (!message?.length || !VALID_CHAT_MODE.includes(mode)) { + response.status(400).json({ + id: uuidv4(), + type: "abort", + textResponse: null, + sources: [], + close: true, + error: !message?.length + ? "Message is empty" + : `${mode} is not a valid mode.`, + }); + return; + } + + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Content-Type", "text/event-stream"); + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Connection", "keep-alive"); + response.flushHeaders(); + + await streamChatWithWorkspace(response, workspace, message, mode); + await Telemetry.sendTelemetry("sent_chat", { + LLMSelection: process.env.LLM_PROVIDER || "openai", + Embedder: process.env.EMBEDDING_ENGINE || "inherit", + VectorDbSelection: process.env.VECTOR_DB || "pinecone", + }); + response.end(); + } catch (e) { + console.error(e); + writeResponseChunk(response, { + id: uuidv4(), + type: "abort", + textResponse: null, + sources: [], + close: true, + error: e.message, + }); + response.end(); + } + } + ); } module.exports = { apiWorkspaceEndpoints }; diff --git a/server/endpoints/chat.js b/server/endpoints/chat.js index d0a2923c5..adfec0ec3 100644 --- a/server/endpoints/chat.js +++ b/server/endpoints/chat.js @@ -1,7 +1,6 @@ const { v4: uuidv4 } = require("uuid"); const { reqBody, userFromSession, multiUserMode } = require("../utils/http"); const { Workspace } = require("../models/workspace"); -const { chatWithWorkspace } = require("../utils/chats"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { WorkspaceChats } = require("../models/workspaceChats"); const { SystemSettings } = require("../models/systemSettings"); @@ -9,6 +8,7 @@ const { Telemetry } = require("../models/telemetry"); const { streamChatWithWorkspace, writeResponseChunk, + VALID_CHAT_MODE, } = require("../utils/chats/stream"); function chatEndpoints(app) { @@ -32,6 +32,20 @@ function chatEndpoints(app) { return; } + if (!message?.length || !VALID_CHAT_MODE.includes(mode)) { + response.status(400).json({ + id: uuidv4(), + type: "abort", + textResponse: null, + sources: [], + close: true, + error: !message?.length + ? "Message is empty." + : `${mode} is not a valid mode.`, + }); + return; + } + response.setHeader("Cache-Control", "no-cache"); response.setHeader("Content-Type", "text/event-stream"); response.setHeader("Access-Control-Allow-Origin", "*"); @@ -95,85 +109,6 @@ function chatEndpoints(app) { } } ); - - app.post( - "/workspace/:slug/chat", - [validatedRequest], - async (request, response) => { - try { - const user = await userFromSession(request, response); - const { slug } = request.params; - const { message, mode = "query" } = reqBody(request); - - const workspace = multiUserMode(response) - ? await Workspace.getWithUser(user, { slug }) - : await Workspace.get({ slug }); - - if (!workspace) { - response.sendStatus(400).end(); - return; - } - - if (multiUserMode(response) && user.role !== "admin") { - const limitMessagesSetting = await SystemSettings.get({ - label: "limit_user_messages", - }); - const limitMessages = limitMessagesSetting?.value === "true"; - - if (limitMessages) { - const messageLimitSetting = await SystemSettings.get({ - label: "message_limit", - }); - const systemLimit = Number(messageLimitSetting?.value); - - if (!!systemLimit) { - const currentChatCount = await WorkspaceChats.count({ - user_id: user.id, - createdAt: { - gte: new Date(new Date() - 24 * 60 * 60 * 1000), - }, - }); - - if (currentChatCount >= systemLimit) { - response.status(500).json({ - id: uuidv4(), - type: "abort", - textResponse: null, - sources: [], - close: true, - error: `You have met your maximum 24 hour chat quota of ${systemLimit} chats set by the instance administrators. Try again later.`, - }); - return; - } - } - } - } - - const result = await chatWithWorkspace(workspace, message, mode, user); - await Telemetry.sendTelemetry( - "sent_chat", - { - multiUserMode: multiUserMode(response), - LLMSelection: process.env.LLM_PROVIDER || "openai", - Embedder: process.env.EMBEDDING_ENGINE || "inherit", - VectorDbSelection: process.env.VECTOR_DB || "pinecone", - }, - user?.id - ); - response.status(200).json({ ...result }); - } catch (e) { - console.error(e); - response.status(500).json({ - id: uuidv4(), - type: "abort", - textResponse: null, - sources: [], - close: true, - error: e.message, - }); - } - } - ); } module.exports = { chatEndpoints }; diff --git a/server/endpoints/invite.js b/server/endpoints/invite.js index 08f9a14e9..4fd8d1545 100644 --- a/server/endpoints/invite.js +++ b/server/endpoints/invite.js @@ -33,7 +33,7 @@ function inviteEndpoints(app) { app.post("/invite/:code", async (request, response) => { try { const { code } = request.params; - const userParams = reqBody(request); + const { username, password } = reqBody(request); const invite = await Invite.get({ code }); if (!invite || invite.status !== "pending") { response @@ -42,7 +42,11 @@ function inviteEndpoints(app) { return; } - const { user, error } = await User.create(userParams); + const { user, error } = await User.create({ + username, + password, + role: "default", + }); if (!user) { console.error("Accepting invite:", error); response diff --git a/server/endpoints/system.js b/server/endpoints/system.js index decde3249..27f6c95cb 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -1,13 +1,14 @@ const path = require("path"); +const fs = require("fs"); process.env.NODE_ENV === "development" ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) : require("dotenv").config({ - path: process.env.STORAGE_DIR - ? path.resolve(process.env.STORAGE_DIR, ".env") - : path.resolve(__dirname, ".env"), - }); + path: process.env.STORAGE_DIR + ? path.resolve(process.env.STORAGE_DIR, ".env") + : path.resolve(__dirname, ".env"), + }); -const { viewLocalFiles } = require("../utils/files"); +const { viewLocalFiles, normalizePath } = require("../utils/files"); const { exportData, unpackAndOverwriteImport } = require("../utils/files/data"); const { checkProcessorAlive, @@ -21,6 +22,7 @@ const { makeJWT, userFromSession, multiUserMode, + queryParams, } = require("../utils/http"); const { setupDataImports, @@ -34,7 +36,6 @@ const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { handleImports } = setupDataImports(); const { handleLogoUploads } = setupLogoUploads(); const { handlePfpUploads } = setupPfpUploads(); -const fs = require("fs"); const { getDefaultFilename, determineLogoFilepath, @@ -111,6 +112,8 @@ function systemEndpoints(app) { app.post("/request-token", async (request, response) => { try { + const bcrypt = require("bcrypt"); + if (await SystemSettings.isMultiUserMode()) { const { username, password } = reqBody(request); const existingUser = await User.get({ username }); @@ -125,7 +128,6 @@ function systemEndpoints(app) { return; } - const bcrypt = require("bcrypt"); if (!bcrypt.compareSync(password, existingUser.password)) { response.status(200).json({ user: null, @@ -163,7 +165,12 @@ function systemEndpoints(app) { return; } else { const { password } = reqBody(request); - if (password !== process.env.AUTH_TOKEN) { + if ( + !bcrypt.compareSync( + password, + bcrypt.hashSync(process.env.AUTH_TOKEN, 10) + ) + ) { response.status(401).json({ valid: false, token: null, @@ -185,16 +192,23 @@ function systemEndpoints(app) { } }); - app.get("/system/system-vectors", [validatedRequest], async (_, response) => { - try { - const VectorDb = getVectorDbClass(); - const vectorCount = await VectorDb.totalVectors(); - response.status(200).json({ vectorCount }); - } catch (e) { - console.log(e.message, e); - response.sendStatus(500).end(); + app.get( + "/system/system-vectors", + [validatedRequest], + async (request, response) => { + try { + const query = queryParams(request); + const VectorDb = getVectorDbClass(); + const vectorCount = !!query.slug + ? await VectorDb.namespaceCount(query.slug) + : await VectorDb.totalVectors(); + response.status(200).json({ vectorCount }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } } - }); + ); app.delete( "/system/remove-document", @@ -274,8 +288,14 @@ function systemEndpoints(app) { [validatedRequest, flexUserRoleValid], async (request, response) => { try { + const user = await userFromSession(request, response); + if (!!user && user.role !== "admin") { + response.sendStatus(401).end(); + return; + } + const body = reqBody(request); - const { newValues, error } = updateENV(body); + const { newValues, error } = await updateENV(body); if (process.env.NODE_ENV === "production") await dumpENV(); response.status(200).json({ newValues, error }); } catch (e) { @@ -297,7 +317,7 @@ function systemEndpoints(app) { } const { usePassword, newPassword } = reqBody(request); - const { error } = updateENV( + const { error } = await updateENV( { AuthToken: usePassword ? newPassword : "", JWTSecret: usePassword ? v4() : "", @@ -340,7 +360,7 @@ function systemEndpoints(app) { message_limit: 25, }); - updateENV( + await updateENV( { AuthToken: "", JWTSecret: process.env.JWT_SECRET || v4(), @@ -374,21 +394,23 @@ function systemEndpoints(app) { } }); - app.get("/system/data-export", [validatedRequest], async (_, response) => { - try { - const { filename, error } = await exportData(); - response.status(200).json({ filename, error }); - } catch (e) { - console.log(e.message, e); - response.sendStatus(500).end(); + app.get( + "/system/data-export", + [validatedRequest, flexUserRoleValid], + async (_, response) => { + try { + const { filename, error } = await exportData(); + response.status(200).json({ filename, error }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } } - }); + ); app.get("/system/data-exports/:filename", (request, response) => { const exportLocation = __dirname + "/../storage/exports/"; - const sanitized = path - .normalize(request.params.filename) - .replace(/^(\.\.(\/|\\|$))+/, ""); + const sanitized = normalizePath(request.params.filename); const finalDestination = path.join(exportLocation, sanitized); if (!fs.existsSync(finalDestination)) { @@ -489,7 +511,8 @@ function systemEndpoints(app) { } const userRecord = await User.get({ id: user.id }); - const oldPfpFilename = userRecord.pfpFilename; + const oldPfpFilename = normalizePath(userRecord.pfpFilename); + console.log("oldPfpFilename", oldPfpFilename); if (oldPfpFilename) { const oldPfpPath = path.join( @@ -523,7 +546,7 @@ function systemEndpoints(app) { try { const user = await userFromSession(request, response); const userRecord = await User.get({ id: user.id }); - const oldPfpFilename = userRecord.pfpFilename; + const oldPfpFilename = normalizePath(userRecord.pfpFilename); console.log("oldPfpFilename", oldPfpFilename); if (oldPfpFilename) { const oldPfpPath = path.join( diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index ac5c3fc11..95b362ca8 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -60,6 +60,19 @@ const SystemSettings = { QdrantApiKey: process.env.QDRANT_API_KEY, } : {}), + ...(vectorDB === "milvus" + ? { + MilvusAddress: process.env.MILVUS_ADDRESS, + MilvusUsername: process.env.MILVUS_USERNAME, + MilvusPassword: !!process.env.MILVUS_PASSWORD, + } + : {}), + ...(vectorDB === "zilliz" + ? { + ZillizEndpoint: process.env.ZILLIZ_ENDPOINT, + ZillizApiToken: process.env.ZILLIZ_API_TOKEN, + } + : {}), LLMProvider: llmProvider, ...(llmProvider === "openai" ? { @@ -144,9 +157,40 @@ const SystemSettings = { AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF, } : {}), + ...(llmProvider === "togetherai" + ? { + TogetherAiApiKey: !!process.env.TOGETHER_AI_API_KEY, + TogetherAiModelPref: process.env.TOGETHER_AI_MODEL_PREF, + + // For embedding credentials when ollama is selected. + OpenAiKey: !!process.env.OPEN_AI_KEY, + AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT, + AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY, + AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF, + } + : {}), + ...(llmProvider === "mistral" + ? { + MistralApiKey: !!process.env.MISTRAL_API_KEY, + MistralModelPref: process.env.MISTRAL_MODEL_PREF, + + // For embedding credentials when mistral is selected. + OpenAiKey: !!process.env.OPEN_AI_KEY, + AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT, + AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY, + AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF, + } + : {}), ...(llmProvider === "native" ? { NativeLLMModelPref: process.env.NATIVE_LLM_MODEL_PREF, + NativeLLMTokenLimit: process.env.NATIVE_LLM_MODEL_TOKEN_LIMIT, + + // For embedding credentials when ollama is selected. + OpenAiKey: !!process.env.OPEN_AI_KEY, + AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT, + AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY, + AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF, } : {}), }; diff --git a/server/models/welcomeMessages.js b/server/models/welcomeMessages.js index 43e2d3f96..88393f36c 100644 --- a/server/models/welcomeMessages.js +++ b/server/models/welcomeMessages.js @@ -31,7 +31,10 @@ const WelcomeMessages = { await prisma.welcome_messages.deleteMany({}); // Delete all existing messages // Create new messages + // We create each message individually because prisma + // with sqlite does not support createMany() for (const [index, message] of messages.entries()) { + if (!message.response) continue; await prisma.welcome_messages.create({ data: { user: message.user, diff --git a/server/models/workspace.js b/server/models/workspace.js index 9139c25e9..6de8053e9 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -14,6 +14,7 @@ const Workspace = { "lastUpdatedAt", "openAiPrompt", "similarityThreshold", + "chatModel", ], new: async function (name = null, creatorId = null) { @@ -191,6 +192,20 @@ const Workspace = { return { success: false, error: error.message }; } }, + + resetWorkspaceChatModels: async () => { + try { + await prisma.workspaces.updateMany({ + data: { + chatModel: null, + }, + }); + return { success: true, error: null }; + } catch (error) { + console.error("Error resetting workspace chat models:", error.message); + return { success: false, error: error.message }; + } + }, }; module.exports = { Workspace }; diff --git a/server/package.json b/server/package.json index 0e2d909c8..9761125a4 100644 --- a/server/package.json +++ b/server/package.json @@ -27,7 +27,8 @@ "@pinecone-database/pinecone": "^0.1.6", "@prisma/client": "5.3.0", "@qdrant/js-client-rest": "^1.4.0", - "@xenova/transformers": "^2.10.0", + "@xenova/transformers": "^2.14.0", + "@zilliz/milvus2-sdk-node": "^2.3.5", "archiver": "^5.3.1", "bcrypt": "^5.1.0", "body-parser": "^1.20.2", diff --git a/server/prisma/migrations/20240113013409_init/migration.sql b/server/prisma/migrations/20240113013409_init/migration.sql new file mode 100644 index 000000000..09b9448ec --- /dev/null +++ b/server/prisma/migrations/20240113013409_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "workspaces" ADD COLUMN "chatModel" TEXT; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 579859d27..8d4d13721 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -93,6 +93,7 @@ model workspaces { lastUpdatedAt DateTime @default(now()) openAiPrompt String? similarityThreshold Float? @default(0.25) + chatModel String? workspace_users workspace_users[] documents workspace_documents[] } diff --git a/server/swagger/init.js b/server/swagger/init.js index c84daf323..b68e3249c 100644 --- a/server/swagger/init.js +++ b/server/swagger/init.js @@ -1,4 +1,6 @@ const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' }); +const fs = require('fs') +const path = require('path') const doc = { info: { @@ -6,6 +8,8 @@ const doc = { title: 'AnythingLLM Developer API', description: 'API endpoints that enable programmatic reading, writing, and updating of your AnythingLLM instance. UI supplied by Swagger.io.', }, + // Swagger-autogen does not allow us to use relative paths as these will resolve to + // http:///api in the openapi.json file, so we need to monkey-patch this post-generation. host: '/api', schemes: ['http'], securityDefinitions: { @@ -25,7 +29,7 @@ const doc = { } }; -const outputFile = './openapi.json'; +const outputFile = path.resolve(__dirname, './openapi.json'); const endpointsFiles = [ '../endpoints/api/auth/index.js', '../endpoints/api/admin/index.js', @@ -34,4 +38,14 @@ const endpointsFiles = [ '../endpoints/api/system/index.js', ]; -swaggerAutogen(outputFile, endpointsFiles, doc) \ No newline at end of file +swaggerAutogen(outputFile, endpointsFiles, doc) + .then(({ data }) => { + const openApiSpec = { + ...data, + servers: [{ + url: "/api" + }] + } + fs.writeFileSync(outputFile, JSON.stringify(openApiSpec, null, 2), { encoding: 'utf-8', flag: 'w' }); + console.log(`Swagger-autogen: \x1b[32mPatched servers.url ✔\x1b[0m`) + }) \ No newline at end of file diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json index cb065522e..c7532059d 100644 --- a/server/swagger/openapi.json +++ b/server/swagger/openapi.json @@ -7,7 +7,7 @@ }, "servers": [ { - "url": "http:///api/" + "url": "/api" } ], "paths": { @@ -845,7 +845,22 @@ "type": "object", "example": { "success": true, - "error": null + "error": null, + "documents": [ + { + "location": "custom-documents/anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json", + "name": "anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json", + "url": "file://Users/tim/Documents/anything-llm/collector/hotdir/anythingllm.txt", + "title": "anythingllm.txt", + "docAuthor": "Unknown", + "description": "Unknown", + "docSource": "a text file uploaded by the user.", + "chunkSource": "anythingllm.txt", + "published": "1/16/2024, 3:07:00 PM", + "wordCount": 93, + "token_count_estimate": 115 + } + ] } } } @@ -890,6 +905,88 @@ } } }, + "/v1/document/upload-link": { + "post": { + "tags": [ + "Documents" + ], + "description": "Upload a valid URL for AnythingLLM to scrape and prepare for embedding.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "success": true, + "error": null, + "documents": [ + { + "id": "c530dbe6-bff1-4b9e-b87f-710d539d20bc", + "url": "file://useanything_com.html", + "title": "useanything_com.html", + "docAuthor": "no author found", + "description": "No description found.", + "docSource": "URL link uploaded by the user.", + "chunkSource": "https:useanything.com.html", + "published": "1/16/2024, 3:46:33 PM", + "wordCount": 252, + "pageContent": "AnythingLLM is the best....", + "token_count_estimate": 447, + "location": "custom-documents/url-useanything_com-c530dbe6-bff1-4b9e-b87f-710d539d20bc.json" + } + ] + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "description": "Link of web address to be scraped.", + "required": true, + "type": "file", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "link": "https://useanything.com" + } + } + } + } + } + } + }, "/v1/documents": { "get": { "tags": [ @@ -953,6 +1050,81 @@ } } }, + "/v1/document/{docName}": { + "get": { + "tags": [ + "Documents" + ], + "description": "Get a single document by its unique AnythingLLM document name", + "parameters": [ + { + "name": "docName", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique document name to find (name in /documents)" + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "localFiles": { + "name": "documents", + "type": "folder", + "items": [ + { + "name": "my-stored-document.txt-uuid1234.json", + "type": "file", + "id": "bb07c334-4dab-4419-9462-9d00065a49a1", + "url": "file://my-stored-document.txt", + "title": "my-stored-document.txt", + "cached": false + } + ] + } + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, "/v1/document/accepted-file-types": { "get": { "tags": [ @@ -1518,9 +1690,11 @@ "content": { "application/json": { "example": { - "adds": [], + "adds": [ + "custom-documents/my-pdf.pdf-hash.json" + ], "deletes": [ - "custom-documents/anythingllm-hash.json" + "custom-documents/anythingllm.txt-hash.json" ] } } @@ -1598,7 +1772,106 @@ } }, "requestBody": { - "description": "prompt to send to the workspace and the type of conversation (query or chat).", + "description": "Send a prompt to the workspace and the type of conversation (query or chat).
Query: Will not use LLM unless there are relevant sources from vectorDB & does not recall chat history.
Chat: Uses LLM general knowledge w/custom embeddings to produce output, uses rolling chat history.", + "required": true, + "type": "object", + "content": { + "application/json": { + "example": { + "message": "What is AnythingLLM?", + "mode": "query | chat" + } + } + } + } + } + }, + "/v1/workspace/{slug}/stream-chat": { + "post": { + "tags": [ + "Workspaces" + ], + "description": "Execute a streamable chat with a workspace", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/event-stream": { + "schema": { + "type": "array", + "example": [ + { + "id": "uuid-123", + "type": "abort | textResponseChunk", + "textResponse": "First chunk", + "sources": [], + "close": false, + "error": "null | text string of the failure mode." + }, + { + "id": "uuid-123", + "type": "abort | textResponseChunk", + "textResponse": "chunk two", + "sources": [], + "close": false, + "error": "null | text string of the failure mode." + }, + { + "id": "uuid-123", + "type": "abort | textResponseChunk", + "textResponse": "final chunk of LLM output!", + "sources": [ + { + "title": "anythingllm.txt", + "chunk": "This is a context chunk used in the answer of the prompt by the LLM. This will only return in the final chunk." + } + ], + "close": true, + "error": "null | text string of the failure mode." + } + ] + } + } + }, + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + } + }, + "requestBody": { + "description": "Send a prompt to the workspace and the type of conversation (query or chat).
Query: Will not use LLM unless there are relevant sources from vectorDB & does not recall chat history.
Chat: Uses LLM general knowledge w/custom embeddings to produce output, uses rolling chat history.", "required": true, "type": "object", "content": { diff --git a/server/utils/AiProviders/anthropic/index.js b/server/utils/AiProviders/anthropic/index.js index 709333231..56d3a80f0 100644 --- a/server/utils/AiProviders/anthropic/index.js +++ b/server/utils/AiProviders/anthropic/index.js @@ -2,7 +2,7 @@ const { v4 } = require("uuid"); const { chatPrompt } = require("../../chats"); class AnthropicLLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { if (!process.env.ANTHROPIC_API_KEY) throw new Error("No Anthropic API key was set."); @@ -12,7 +12,8 @@ class AnthropicLLM { apiKey: process.env.ANTHROPIC_API_KEY, }); this.anthropic = anthropic; - this.model = process.env.ANTHROPIC_MODEL_PREF || "claude-2"; + this.model = + modelPreference || process.env.ANTHROPIC_MODEL_PREF || "claude-2"; this.limits = { history: this.promptWindowLimit() * 0.15, system: this.promptWindowLimit() * 0.15, @@ -25,6 +26,7 @@ class AnthropicLLM { ); this.embedder = embedder; this.answerKey = v4().split("-")[0]; + this.defaultTemp = 0.7; } streamingEnabled() { diff --git a/server/utils/AiProviders/azureOpenAi/index.js b/server/utils/AiProviders/azureOpenAi/index.js index 185dac021..639ac102e 100644 --- a/server/utils/AiProviders/azureOpenAi/index.js +++ b/server/utils/AiProviders/azureOpenAi/index.js @@ -2,7 +2,7 @@ const { AzureOpenAiEmbedder } = require("../../EmbeddingEngines/azureOpenAi"); const { chatPrompt } = require("../../chats"); class AzureOpenAiLLM { - constructor(embedder = null) { + constructor(embedder = null, _modelPreference = null) { const { OpenAIClient, AzureKeyCredential } = require("@azure/openai"); if (!process.env.AZURE_OPENAI_ENDPOINT) throw new Error("No Azure API endpoint was set."); @@ -25,6 +25,7 @@ class AzureOpenAiLLM { "No embedding provider defined for AzureOpenAiLLM - falling back to AzureOpenAiEmbedder for embedding!" ); this.embedder = !embedder ? new AzureOpenAiEmbedder() : embedder; + this.defaultTemp = 0.7; } #appendContext(contextTexts = []) { @@ -93,7 +94,7 @@ class AzureOpenAiLLM { ); const textResponse = await this.openai .getChatCompletions(this.model, messages, { - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, }) .then((res) => { @@ -130,7 +131,7 @@ class AzureOpenAiLLM { this.model, messages, { - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, } ); diff --git a/server/utils/AiProviders/gemini/index.js b/server/utils/AiProviders/gemini/index.js index 03388e3e2..63549fb8d 100644 --- a/server/utils/AiProviders/gemini/index.js +++ b/server/utils/AiProviders/gemini/index.js @@ -1,14 +1,15 @@ const { chatPrompt } = require("../../chats"); class GeminiLLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { if (!process.env.GEMINI_API_KEY) throw new Error("No Gemini API key was set."); // Docs: https://ai.google.dev/tutorials/node_quickstart const { GoogleGenerativeAI } = require("@google/generative-ai"); const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); - this.model = process.env.GEMINI_LLM_MODEL_PREF || "gemini-pro"; + this.model = + modelPreference || process.env.GEMINI_LLM_MODEL_PREF || "gemini-pro"; this.gemini = genAI.getGenerativeModel({ model: this.model }); this.limits = { history: this.promptWindowLimit() * 0.15, @@ -21,6 +22,7 @@ class GeminiLLM { "INVALID GEMINI LLM SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use Gemini as your LLM." ); this.embedder = embedder; + this.defaultTemp = 0.7; // not used for Gemini } #appendContext(contextTexts = []) { diff --git a/server/utils/AiProviders/lmStudio/index.js b/server/utils/AiProviders/lmStudio/index.js index 28c107df0..08950a7b9 100644 --- a/server/utils/AiProviders/lmStudio/index.js +++ b/server/utils/AiProviders/lmStudio/index.js @@ -2,7 +2,7 @@ const { chatPrompt } = require("../../chats"); // hybrid of openAi LLM chat completion for LMStudio class LMStudioLLM { - constructor(embedder = null) { + constructor(embedder = null, _modelPreference = null) { if (!process.env.LMSTUDIO_BASE_PATH) throw new Error("No LMStudio API Base Path was set."); @@ -12,7 +12,7 @@ class LMStudioLLM { }); this.lmstudio = new OpenAIApi(config); // When using LMStudios inference server - the model param is not required so - // we can stub it here. + // we can stub it here. LMStudio can only run one model at a time. this.model = "model-placeholder"; this.limits = { history: this.promptWindowLimit() * 0.15, @@ -25,6 +25,7 @@ class LMStudioLLM { "INVALID LM STUDIO SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use LMStudio as your LLM." ); this.embedder = embedder; + this.defaultTemp = 0.7; } #appendContext(contextTexts = []) { @@ -85,7 +86,7 @@ class LMStudioLLM { const textResponse = await this.lmstudio .createChatCompletion({ model: this.model, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, messages: await this.compressMessages( { @@ -122,7 +123,7 @@ class LMStudioLLM { const streamRequest = await this.lmstudio.createChatCompletion( { model: this.model, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, stream: true, messages: await this.compressMessages( diff --git a/server/utils/AiProviders/localAi/index.js b/server/utils/AiProviders/localAi/index.js index 84954c994..6d265cf82 100644 --- a/server/utils/AiProviders/localAi/index.js +++ b/server/utils/AiProviders/localAi/index.js @@ -1,7 +1,7 @@ const { chatPrompt } = require("../../chats"); class LocalAiLLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { if (!process.env.LOCAL_AI_BASE_PATH) throw new Error("No LocalAI Base Path was set."); @@ -15,7 +15,7 @@ class LocalAiLLM { : {}), }); this.openai = new OpenAIApi(config); - this.model = process.env.LOCAL_AI_MODEL_PREF; + this.model = modelPreference || process.env.LOCAL_AI_MODEL_PREF; this.limits = { history: this.promptWindowLimit() * 0.15, system: this.promptWindowLimit() * 0.15, @@ -27,6 +27,7 @@ class LocalAiLLM { "INVALID LOCAL AI SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use LocalAI as your LLM." ); this.embedder = embedder; + this.defaultTemp = 0.7; } #appendContext(contextTexts = []) { @@ -85,7 +86,7 @@ class LocalAiLLM { const textResponse = await this.openai .createChatCompletion({ model: this.model, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, messages: await this.compressMessages( { @@ -123,7 +124,7 @@ class LocalAiLLM { { model: this.model, stream: true, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, messages: await this.compressMessages( { diff --git a/server/utils/AiProviders/mistral/index.js b/server/utils/AiProviders/mistral/index.js new file mode 100644 index 000000000..a25185c76 --- /dev/null +++ b/server/utils/AiProviders/mistral/index.js @@ -0,0 +1,184 @@ +const { chatPrompt } = require("../../chats"); + +class MistralLLM { + constructor(embedder = null, modelPreference = null) { + const { Configuration, OpenAIApi } = require("openai"); + if (!process.env.MISTRAL_API_KEY) + throw new Error("No Mistral API key was set."); + + const config = new Configuration({ + basePath: "https://api.mistral.ai/v1", + apiKey: process.env.MISTRAL_API_KEY, + }); + this.openai = new OpenAIApi(config); + this.model = + modelPreference || process.env.MISTRAL_MODEL_PREF || "mistral-tiny"; + this.limits = { + history: this.promptWindowLimit() * 0.15, + system: this.promptWindowLimit() * 0.15, + user: this.promptWindowLimit() * 0.7, + }; + + if (!embedder) + console.warn( + "No embedding provider defined for MistralLLM - falling back to OpenAiEmbedder for embedding!" + ); + this.embedder = embedder; + this.defaultTemp = 0.0; + } + + #appendContext(contextTexts = []) { + if (!contextTexts || !contextTexts.length) return ""; + return ( + "\nContext:\n" + + contextTexts + .map((text, i) => { + return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`; + }) + .join("") + ); + } + + streamingEnabled() { + return "streamChat" in this && "streamGetChatCompletion" in this; + } + + promptWindowLimit() { + return 32000; + } + + async isValidChatCompletionModel(modelName = "") { + return true; + } + + constructPrompt({ + systemPrompt = "", + contextTexts = [], + chatHistory = [], + userPrompt = "", + }) { + const prompt = { + role: "system", + content: `${systemPrompt}${this.#appendContext(contextTexts)}`, + }; + return [prompt, ...chatHistory, { role: "user", content: userPrompt }]; + } + + async isSafe(_ = "") { + return { safe: true, reasons: [] }; + } + + async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `Mistral chat: ${this.model} is not valid for chat completion!` + ); + + const textResponse = await this.openai + .createChatCompletion({ + model: this.model, + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), + messages: await this.compressMessages( + { + systemPrompt: chatPrompt(workspace), + userPrompt: prompt, + chatHistory, + }, + rawHistory + ), + }) + .then((json) => { + const res = json.data; + if (!res.hasOwnProperty("choices")) + throw new Error("Mistral chat: No results!"); + if (res.choices.length === 0) + throw new Error("Mistral chat: No results length!"); + return res.choices[0].message.content; + }) + .catch((error) => { + throw new Error( + `Mistral::createChatCompletion failed with: ${error.message}` + ); + }); + + return textResponse; + } + + async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `Mistral chat: ${this.model} is not valid for chat completion!` + ); + + const streamRequest = await this.openai.createChatCompletion( + { + model: this.model, + stream: true, + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), + messages: await this.compressMessages( + { + systemPrompt: chatPrompt(workspace), + userPrompt: prompt, + chatHistory, + }, + rawHistory + ), + }, + { responseType: "stream" } + ); + + return streamRequest; + } + + async getChatCompletion(messages = null, { temperature = 0.7 }) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `Mistral chat: ${this.model} is not valid for chat completion!` + ); + + const { data } = await this.openai.createChatCompletion({ + model: this.model, + messages, + temperature, + }); + + if (!data.hasOwnProperty("choices")) return null; + return data.choices[0].message.content; + } + + async streamGetChatCompletion(messages = null, { temperature = 0.7 }) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `Mistral chat: ${this.model} is not valid for chat completion!` + ); + + const streamRequest = await this.openai.createChatCompletion( + { + model: this.model, + stream: true, + messages, + temperature, + }, + { responseType: "stream" } + ); + return streamRequest; + } + + // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations + async embedTextInput(textInput) { + return await this.embedder.embedTextInput(textInput); + } + async embedChunks(textChunks = []) { + return await this.embedder.embedChunks(textChunks); + } + + async compressMessages(promptArgs = {}, rawHistory = []) { + const { messageArrayCompressor } = require("../../helpers/chat"); + const messageArray = this.constructPrompt(promptArgs); + return await messageArrayCompressor(this, messageArray, rawHistory); + } +} + +module.exports = { + MistralLLM, +}; diff --git a/server/utils/AiProviders/native/index.js b/server/utils/AiProviders/native/index.js index faac4fa03..de1a97f3d 100644 --- a/server/utils/AiProviders/native/index.js +++ b/server/utils/AiProviders/native/index.js @@ -10,11 +10,11 @@ const ChatLlamaCpp = (...args) => ); class NativeLLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { if (!process.env.NATIVE_LLM_MODEL_PREF) throw new Error("No local Llama model was set."); - this.model = process.env.NATIVE_LLM_MODEL_PREF || null; + this.model = modelPreference || process.env.NATIVE_LLM_MODEL_PREF || null; this.limits = { history: this.promptWindowLimit() * 0.15, system: this.promptWindowLimit() * 0.15, @@ -29,6 +29,7 @@ class NativeLLM { // Make directory when it does not exist in existing installations if (!fs.existsSync(this.cacheDir)) fs.mkdirSync(this.cacheDir); + this.defaultTemp = 0.7; } async #initializeLlamaModel(temperature = 0.7) { @@ -93,8 +94,6 @@ class NativeLLM { } // Ensure the user set a value for the token limit - // and if undefined - assume 4096 window. - // DEV: Currently this ENV is not configurable. promptWindowLimit() { const limit = process.env.NATIVE_LLM_MODEL_TOKEN_LIMIT || 4096; if (!limit || isNaN(Number(limit))) @@ -132,7 +131,7 @@ class NativeLLM { ); const model = await this.#llamaClient({ - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), }); const response = await model.call(messages); return response.content; @@ -145,7 +144,7 @@ class NativeLLM { async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { const model = await this.#llamaClient({ - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), }); const messages = await this.compressMessages( { diff --git a/server/utils/AiProviders/ollama/index.js b/server/utils/AiProviders/ollama/index.js index 55205c23d..af7fe8210 100644 --- a/server/utils/AiProviders/ollama/index.js +++ b/server/utils/AiProviders/ollama/index.js @@ -3,12 +3,12 @@ const { StringOutputParser } = require("langchain/schema/output_parser"); // Docs: https://github.com/jmorganca/ollama/blob/main/docs/api.md class OllamaAILLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { if (!process.env.OLLAMA_BASE_PATH) throw new Error("No Ollama Base Path was set."); this.basePath = process.env.OLLAMA_BASE_PATH; - this.model = process.env.OLLAMA_MODEL_PREF; + this.model = modelPreference || process.env.OLLAMA_MODEL_PREF; this.limits = { history: this.promptWindowLimit() * 0.15, system: this.promptWindowLimit() * 0.15, @@ -20,6 +20,7 @@ class OllamaAILLM { "INVALID OLLAMA SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use Ollama as your LLM." ); this.embedder = embedder; + this.defaultTemp = 0.7; } #ollamaClient({ temperature = 0.07 }) { @@ -113,7 +114,7 @@ class OllamaAILLM { ); const model = this.#ollamaClient({ - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), }); const textResponse = await model .pipe(new StringOutputParser()) @@ -136,7 +137,7 @@ class OllamaAILLM { ); const model = this.#ollamaClient({ - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), }); const stream = await model .pipe(new StringOutputParser()) diff --git a/server/utils/AiProviders/openAi/index.js b/server/utils/AiProviders/openAi/index.js index ccc7ba0e9..582bc054d 100644 --- a/server/utils/AiProviders/openAi/index.js +++ b/server/utils/AiProviders/openAi/index.js @@ -2,7 +2,7 @@ const { OpenAiEmbedder } = require("../../EmbeddingEngines/openAi"); const { chatPrompt } = require("../../chats"); class OpenAiLLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { const { Configuration, OpenAIApi } = require("openai"); if (!process.env.OPEN_AI_KEY) throw new Error("No OpenAI API key was set."); @@ -10,7 +10,8 @@ class OpenAiLLM { apiKey: process.env.OPEN_AI_KEY, }); this.openai = new OpenAIApi(config); - this.model = process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; + this.model = + modelPreference || process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; this.limits = { history: this.promptWindowLimit() * 0.15, system: this.promptWindowLimit() * 0.15, @@ -22,6 +23,7 @@ class OpenAiLLM { "No embedding provider defined for OpenAiLLM - falling back to OpenAiEmbedder for embedding!" ); this.embedder = !embedder ? new OpenAiEmbedder() : embedder; + this.defaultTemp = 0.7; } #appendContext(contextTexts = []) { @@ -126,7 +128,7 @@ class OpenAiLLM { const textResponse = await this.openai .createChatCompletion({ model: this.model, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, messages: await this.compressMessages( { @@ -164,7 +166,7 @@ class OpenAiLLM { { model: this.model, stream: true, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, messages: await this.compressMessages( { diff --git a/server/utils/AiProviders/togetherAi/index.js b/server/utils/AiProviders/togetherAi/index.js new file mode 100644 index 000000000..341661f8d --- /dev/null +++ b/server/utils/AiProviders/togetherAi/index.js @@ -0,0 +1,199 @@ +const { chatPrompt } = require("../../chats"); + +function togetherAiModels() { + const { MODELS } = require("./models.js"); + return MODELS || {}; +} + +class TogetherAiLLM { + constructor(embedder = null, modelPreference = null) { + const { Configuration, OpenAIApi } = require("openai"); + if (!process.env.TOGETHER_AI_API_KEY) + throw new Error("No TogetherAI API key was set."); + + const config = new Configuration({ + basePath: "https://api.together.xyz/v1", + apiKey: process.env.TOGETHER_AI_API_KEY, + }); + this.openai = new OpenAIApi(config); + this.model = modelPreference || process.env.TOGETHER_AI_MODEL_PREF; + this.limits = { + history: this.promptWindowLimit() * 0.15, + system: this.promptWindowLimit() * 0.15, + user: this.promptWindowLimit() * 0.7, + }; + + if (!embedder) + throw new Error( + "INVALID TOGETHER AI SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use Together AI as your LLM." + ); + this.embedder = embedder; + this.defaultTemp = 0.7; + } + + #appendContext(contextTexts = []) { + if (!contextTexts || !contextTexts.length) return ""; + return ( + "\nContext:\n" + + contextTexts + .map((text, i) => { + return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`; + }) + .join("") + ); + } + + allModelInformation() { + return togetherAiModels(); + } + + streamingEnabled() { + return "streamChat" in this && "streamGetChatCompletion" in this; + } + + // Ensure the user set a value for the token limit + // and if undefined - assume 4096 window. + promptWindowLimit() { + const availableModels = this.allModelInformation(); + return availableModels[this.model]?.maxLength || 4096; + } + + async isValidChatCompletionModel(model = "") { + const availableModels = this.allModelInformation(); + return availableModels.hasOwnProperty(model); + } + + constructPrompt({ + systemPrompt = "", + contextTexts = [], + chatHistory = [], + userPrompt = "", + }) { + const prompt = { + role: "system", + content: `${systemPrompt}${this.#appendContext(contextTexts)}`, + }; + return [prompt, ...chatHistory, { role: "user", content: userPrompt }]; + } + + async isSafe(_input = "") { + // Not implemented so must be stubbed + return { safe: true, reasons: [] }; + } + + async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `Together AI chat: ${this.model} is not valid for chat completion!` + ); + + const textResponse = await this.openai + .createChatCompletion({ + model: this.model, + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), + n: 1, + messages: await this.compressMessages( + { + systemPrompt: chatPrompt(workspace), + userPrompt: prompt, + chatHistory, + }, + rawHistory + ), + }) + .then((json) => { + const res = json.data; + if (!res.hasOwnProperty("choices")) + throw new Error("Together AI chat: No results!"); + if (res.choices.length === 0) + throw new Error("Together AI chat: No results length!"); + return res.choices[0].message.content; + }) + .catch((error) => { + throw new Error( + `TogetherAI::createChatCompletion failed with: ${error.message}` + ); + }); + + return textResponse; + } + + async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `TogetherAI chat: ${this.model} is not valid for chat completion!` + ); + + const streamRequest = await this.openai.createChatCompletion( + { + model: this.model, + stream: true, + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), + n: 1, + messages: await this.compressMessages( + { + systemPrompt: chatPrompt(workspace), + userPrompt: prompt, + chatHistory, + }, + rawHistory + ), + }, + { responseType: "stream" } + ); + return { type: "togetherAiStream", stream: streamRequest }; + } + + async getChatCompletion(messages = null, { temperature = 0.7 }) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `TogetherAI chat: ${this.model} is not valid for chat completion!` + ); + + const { data } = await this.openai.createChatCompletion({ + model: this.model, + messages, + temperature, + }); + + if (!data.hasOwnProperty("choices")) return null; + return data.choices[0].message.content; + } + + async streamGetChatCompletion(messages = null, { temperature = 0.7 }) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `TogetherAI chat: ${this.model} is not valid for chat completion!` + ); + + const streamRequest = await this.openai.createChatCompletion( + { + model: this.model, + stream: true, + messages, + temperature, + }, + { responseType: "stream" } + ); + return { type: "togetherAiStream", stream: streamRequest }; + } + + // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations + async embedTextInput(textInput) { + return await this.embedder.embedTextInput(textInput); + } + async embedChunks(textChunks = []) { + return await this.embedder.embedChunks(textChunks); + } + + async compressMessages(promptArgs = {}, rawHistory = []) { + const { messageArrayCompressor } = require("../../helpers/chat"); + const messageArray = this.constructPrompt(promptArgs); + return await messageArrayCompressor(this, messageArray, rawHistory); + } +} + +module.exports = { + TogetherAiLLM, + togetherAiModels, +}; diff --git a/server/utils/AiProviders/togetherAi/models.js b/server/utils/AiProviders/togetherAi/models.js new file mode 100644 index 000000000..ad940bc39 --- /dev/null +++ b/server/utils/AiProviders/togetherAi/models.js @@ -0,0 +1,226 @@ +const MODELS = { + "togethercomputer/alpaca-7b": { + id: "togethercomputer/alpaca-7b", + organization: "Stanford", + name: "Alpaca (7B)", + maxLength: 2048, + }, + "Austism/chronos-hermes-13b": { + id: "Austism/chronos-hermes-13b", + organization: "Austism", + name: "Chronos Hermes (13B)", + maxLength: 2048, + }, + "togethercomputer/CodeLlama-13b-Instruct": { + id: "togethercomputer/CodeLlama-13b-Instruct", + organization: "Meta", + name: "Code Llama Instruct (13B)", + maxLength: 8192, + }, + "togethercomputer/CodeLlama-34b-Instruct": { + id: "togethercomputer/CodeLlama-34b-Instruct", + organization: "Meta", + name: "Code Llama Instruct (34B)", + maxLength: 8192, + }, + "togethercomputer/CodeLlama-7b-Instruct": { + id: "togethercomputer/CodeLlama-7b-Instruct", + organization: "Meta", + name: "Code Llama Instruct (7B)", + maxLength: 8192, + }, + "DiscoResearch/DiscoLM-mixtral-8x7b-v2": { + id: "DiscoResearch/DiscoLM-mixtral-8x7b-v2", + organization: "DiscoResearch", + name: "DiscoLM Mixtral 8x7b", + maxLength: 32768, + }, + "togethercomputer/falcon-40b-instruct": { + id: "togethercomputer/falcon-40b-instruct", + organization: "TII UAE", + name: "Falcon Instruct (40B)", + maxLength: 2048, + }, + "togethercomputer/falcon-7b-instruct": { + id: "togethercomputer/falcon-7b-instruct", + organization: "TII UAE", + name: "Falcon Instruct (7B)", + maxLength: 2048, + }, + "togethercomputer/GPT-NeoXT-Chat-Base-20B": { + id: "togethercomputer/GPT-NeoXT-Chat-Base-20B", + organization: "Together", + name: "GPT-NeoXT-Chat-Base (20B)", + maxLength: 2048, + }, + "togethercomputer/llama-2-13b-chat": { + id: "togethercomputer/llama-2-13b-chat", + organization: "Meta", + name: "LLaMA-2 Chat (13B)", + maxLength: 4096, + }, + "togethercomputer/llama-2-70b-chat": { + id: "togethercomputer/llama-2-70b-chat", + organization: "Meta", + name: "LLaMA-2 Chat (70B)", + maxLength: 4096, + }, + "togethercomputer/llama-2-7b-chat": { + id: "togethercomputer/llama-2-7b-chat", + organization: "Meta", + name: "LLaMA-2 Chat (7B)", + maxLength: 4096, + }, + "togethercomputer/Llama-2-7B-32K-Instruct": { + id: "togethercomputer/Llama-2-7B-32K-Instruct", + organization: "Together", + name: "LLaMA-2-7B-32K-Instruct (7B)", + maxLength: 32768, + }, + "mistralai/Mistral-7B-Instruct-v0.1": { + id: "mistralai/Mistral-7B-Instruct-v0.1", + organization: "MistralAI", + name: "Mistral (7B) Instruct v0.1", + maxLength: 4096, + }, + "mistralai/Mistral-7B-Instruct-v0.2": { + id: "mistralai/Mistral-7B-Instruct-v0.2", + organization: "MistralAI", + name: "Mistral (7B) Instruct v0.2", + maxLength: 32768, + }, + "mistralai/Mixtral-8x7B-Instruct-v0.1": { + id: "mistralai/Mixtral-8x7B-Instruct-v0.1", + organization: "MistralAI", + name: "Mixtral-8x7B Instruct", + maxLength: 32768, + }, + "Gryphe/MythoMax-L2-13b": { + id: "Gryphe/MythoMax-L2-13b", + organization: "Gryphe", + name: "MythoMax-L2 (13B)", + maxLength: 4096, + }, + "NousResearch/Nous-Hermes-llama-2-7b": { + id: "NousResearch/Nous-Hermes-llama-2-7b", + organization: "NousResearch", + name: "Nous Hermes LLaMA-2 (7B)", + maxLength: 4096, + }, + "NousResearch/Nous-Hermes-Llama2-13b": { + id: "NousResearch/Nous-Hermes-Llama2-13b", + organization: "NousResearch", + name: "Nous Hermes Llama-2 (13B)", + maxLength: 4096, + }, + "NousResearch/Nous-Hermes-Llama2-70b": { + id: "NousResearch/Nous-Hermes-Llama2-70b", + organization: "NousResearch", + name: "Nous Hermes Llama-2 (70B)", + maxLength: 4096, + }, + "NousResearch/Nous-Hermes-2-Yi-34B": { + id: "NousResearch/Nous-Hermes-2-Yi-34B", + organization: "NousResearch", + name: "Nous Hermes-2 Yi (34B)", + maxLength: 4096, + }, + "NousResearch/Nous-Capybara-7B-V1p9": { + id: "NousResearch/Nous-Capybara-7B-V1p9", + organization: "NousResearch", + name: "Nous Capybara v1.9 (7B)", + maxLength: 8192, + }, + "openchat/openchat-3.5-1210": { + id: "openchat/openchat-3.5-1210", + organization: "OpenChat", + name: "OpenChat 3.5 1210 (7B)", + maxLength: 8192, + }, + "teknium/OpenHermes-2-Mistral-7B": { + id: "teknium/OpenHermes-2-Mistral-7B", + organization: "teknium", + name: "OpenHermes-2-Mistral (7B)", + maxLength: 4096, + }, + "teknium/OpenHermes-2p5-Mistral-7B": { + id: "teknium/OpenHermes-2p5-Mistral-7B", + organization: "teknium", + name: "OpenHermes-2.5-Mistral (7B)", + maxLength: 4096, + }, + "Open-Orca/Mistral-7B-OpenOrca": { + id: "Open-Orca/Mistral-7B-OpenOrca", + organization: "OpenOrca", + name: "OpenOrca Mistral (7B) 8K", + maxLength: 8192, + }, + "garage-bAInd/Platypus2-70B-instruct": { + id: "garage-bAInd/Platypus2-70B-instruct", + organization: "garage-bAInd", + name: "Platypus2 Instruct (70B)", + maxLength: 4096, + }, + "togethercomputer/Pythia-Chat-Base-7B-v0.16": { + id: "togethercomputer/Pythia-Chat-Base-7B-v0.16", + organization: "Together", + name: "Pythia-Chat-Base (7B)", + maxLength: 2048, + }, + "togethercomputer/Qwen-7B-Chat": { + id: "togethercomputer/Qwen-7B-Chat", + organization: "Qwen", + name: "Qwen-Chat (7B)", + maxLength: 8192, + }, + "togethercomputer/RedPajama-INCITE-Chat-3B-v1": { + id: "togethercomputer/RedPajama-INCITE-Chat-3B-v1", + organization: "Together", + name: "RedPajama-INCITE Chat (3B)", + maxLength: 2048, + }, + "togethercomputer/RedPajama-INCITE-7B-Chat": { + id: "togethercomputer/RedPajama-INCITE-7B-Chat", + organization: "Together", + name: "RedPajama-INCITE Chat (7B)", + maxLength: 2048, + }, + "upstage/SOLAR-0-70b-16bit": { + id: "upstage/SOLAR-0-70b-16bit", + organization: "Upstage", + name: "SOLAR v0 (70B)", + maxLength: 4096, + }, + "togethercomputer/StripedHyena-Nous-7B": { + id: "togethercomputer/StripedHyena-Nous-7B", + organization: "Together", + name: "StripedHyena Nous (7B)", + maxLength: 32768, + }, + "lmsys/vicuna-7b-v1.5": { + id: "lmsys/vicuna-7b-v1.5", + organization: "LM Sys", + name: "Vicuna v1.5 (7B)", + maxLength: 4096, + }, + "lmsys/vicuna-13b-v1.5": { + id: "lmsys/vicuna-13b-v1.5", + organization: "LM Sys", + name: "Vicuna v1.5 (13B)", + maxLength: 4096, + }, + "lmsys/vicuna-13b-v1.5-16k": { + id: "lmsys/vicuna-13b-v1.5-16k", + organization: "LM Sys", + name: "Vicuna v1.5 16K (13B)", + maxLength: 16384, + }, + "zero-one-ai/Yi-34B-Chat": { + id: "zero-one-ai/Yi-34B-Chat", + organization: "01.AI", + name: "01-ai Yi Chat (34B)", + maxLength: 4096, + }, +}; + +module.exports.MODELS = MODELS; diff --git a/server/utils/AiProviders/togetherAi/scripts/.gitignore b/server/utils/AiProviders/togetherAi/scripts/.gitignore new file mode 100644 index 000000000..94a2dd146 --- /dev/null +++ b/server/utils/AiProviders/togetherAi/scripts/.gitignore @@ -0,0 +1 @@ +*.json \ No newline at end of file diff --git a/server/utils/AiProviders/togetherAi/scripts/chat_models.txt b/server/utils/AiProviders/togetherAi/scripts/chat_models.txt new file mode 100644 index 000000000..81c23bf4a --- /dev/null +++ b/server/utils/AiProviders/togetherAi/scripts/chat_models.txt @@ -0,0 +1,39 @@ +| Organization | Model Name | Model String for API | Max Seq Length | +| ------------- | ---------------------------- | -------------------------------------------- | -------------- | +| Stanford | Alpaca (7B) | togethercomputer/alpaca-7b | 2048 | +| Austism | Chronos Hermes (13B) | Austism/chronos-hermes-13b | 2048 | +| Meta | Code Llama Instruct (13B) | togethercomputer/CodeLlama-13b-Instruct | 8192 | +| Meta | Code Llama Instruct (34B) | togethercomputer/CodeLlama-34b-Instruct | 8192 | +| Meta | Code Llama Instruct (7B) | togethercomputer/CodeLlama-7b-Instruct | 8192 | +| DiscoResearch | DiscoLM Mixtral 8x7b | DiscoResearch/DiscoLM-mixtral-8x7b-v2 | 32768 | +| TII UAE | Falcon Instruct (40B) | togethercomputer/falcon-40b-instruct | 2048 | +| TII UAE | Falcon Instruct (7B) | togethercomputer/falcon-7b-instruct | 2048 | +| Together | GPT-NeoXT-Chat-Base (20B) | togethercomputer/GPT-NeoXT-Chat-Base-20B | 2048 | +| Meta | LLaMA-2 Chat (13B) | togethercomputer/llama-2-13b-chat | 4096 | +| Meta | LLaMA-2 Chat (70B) | togethercomputer/llama-2-70b-chat | 4096 | +| Meta | LLaMA-2 Chat (7B) | togethercomputer/llama-2-7b-chat | 4096 | +| Together | LLaMA-2-7B-32K-Instruct (7B) | togethercomputer/Llama-2-7B-32K-Instruct | 32768 | +| MistralAI | Mistral (7B) Instruct v0.1 | mistralai/Mistral-7B-Instruct-v0.1 | 4096 | +| MistralAI | Mistral (7B) Instruct v0.2 | mistralai/Mistral-7B-Instruct-v0.2 | 32768 | +| MistralAI | Mixtral-8x7B Instruct | mistralai/Mixtral-8x7B-Instruct-v0.1 | 32768 | +| Gryphe | MythoMax-L2 (13B) | Gryphe/MythoMax-L2-13b | 4096 | +| NousResearch | Nous Hermes LLaMA-2 (7B) | NousResearch/Nous-Hermes-llama-2-7b | 4096 | +| NousResearch | Nous Hermes Llama-2 (13B) | NousResearch/Nous-Hermes-Llama2-13b | 4096 | +| NousResearch | Nous Hermes Llama-2 (70B) | NousResearch/Nous-Hermes-Llama2-70b | 4096 | +| NousResearch | Nous Hermes-2 Yi (34B) | NousResearch/Nous-Hermes-2-Yi-34B | 4096 | +| NousResearch | Nous Capybara v1.9 (7B) | NousResearch/Nous-Capybara-7B-V1p9 | 8192 | +| OpenChat | OpenChat 3.5 1210 (7B) | openchat/openchat-3.5-1210 | 8192 | +| teknium | OpenHermes-2-Mistral (7B) | teknium/OpenHermes-2-Mistral-7B | 4096 | +| teknium | OpenHermes-2.5-Mistral (7B) | teknium/OpenHermes-2p5-Mistral-7B | 4096 | +| OpenOrca | OpenOrca Mistral (7B) 8K | Open-Orca/Mistral-7B-OpenOrca | 8192 | +| garage-bAInd | Platypus2 Instruct (70B) | garage-bAInd/Platypus2-70B-instruct | 4096 | +| Together | Pythia-Chat-Base (7B) | togethercomputer/Pythia-Chat-Base-7B-v0.16 | 2048 | +| Qwen | Qwen-Chat (7B) | togethercomputer/Qwen-7B-Chat | 8192 | +| Together | RedPajama-INCITE Chat (3B) | togethercomputer/RedPajama-INCITE-Chat-3B-v1 | 2048 | +| Together | RedPajama-INCITE Chat (7B) | togethercomputer/RedPajama-INCITE-7B-Chat | 2048 | +| Upstage | SOLAR v0 (70B) | upstage/SOLAR-0-70b-16bit | 4096 | +| Together | StripedHyena Nous (7B) | togethercomputer/StripedHyena-Nous-7B | 32768 | +| LM Sys | Vicuna v1.5 (7B) | lmsys/vicuna-7b-v1.5 | 4096 | +| LM Sys | Vicuna v1.5 (13B) | lmsys/vicuna-13b-v1.5 | 4096 | +| LM Sys | Vicuna v1.5 16K (13B) | lmsys/vicuna-13b-v1.5-16k | 16384 | +| 01.AI | 01-ai Yi Chat (34B) | zero-one-ai/Yi-34B-Chat | 4096 | \ No newline at end of file diff --git a/server/utils/AiProviders/togetherAi/scripts/parse.mjs b/server/utils/AiProviders/togetherAi/scripts/parse.mjs new file mode 100644 index 000000000..b96d40ab1 --- /dev/null +++ b/server/utils/AiProviders/togetherAi/scripts/parse.mjs @@ -0,0 +1,41 @@ +// Together AI does not provide a simple REST API to get models, +// so we have a table which we copy from their documentation +// https://docs.together.ai/edit/inference-models that we can +// then parse and get all models from in a format that makes sense +// Why this does not exist is so bizarre, but whatever. + +// To run, cd into this directory and run `node parse.mjs` +// copy outputs into the export in ../models.js + +// Update the date below if you run this again because TogetherAI added new models. +// Last Collected: Jan 10, 2023 + +import fs from "fs"; + +function parseChatModels() { + const fixed = {}; + const tableString = fs.readFileSync("chat_models.txt", { encoding: "utf-8" }); + const rows = tableString.split("\n").slice(2); + + rows.forEach((row) => { + const [provider, name, id, maxLength] = row.split("|").slice(1, -1); + const data = { + provider: provider.trim(), + name: name.trim(), + id: id.trim(), + maxLength: Number(maxLength.trim()), + }; + + fixed[data.id] = { + id: data.id, + organization: data.provider, + name: data.name, + maxLength: data.maxLength, + }; + }); + + fs.writeFileSync("chat_models.json", JSON.stringify(fixed, null, 2), "utf-8"); + return fixed; +} + +parseChatModels(); diff --git a/server/utils/EmbeddingEngines/native/index.js b/server/utils/EmbeddingEngines/native/index.js index 69e13a9e3..fc933e1b8 100644 --- a/server/utils/EmbeddingEngines/native/index.js +++ b/server/utils/EmbeddingEngines/native/index.js @@ -1,6 +1,7 @@ const path = require("path"); const fs = require("fs"); const { toChunks } = require("../../helpers"); +const { v4 } = require("uuid"); class NativeEmbedder { constructor() { @@ -14,13 +15,30 @@ class NativeEmbedder { this.modelPath = path.resolve(this.cacheDir, "Xenova", "all-MiniLM-L6-v2"); // Limit of how many strings we can process in a single pass to stay with resource or network limits - this.maxConcurrentChunks = 50; + this.maxConcurrentChunks = 25; this.embeddingMaxChunkLength = 1_000; // Make directory when it does not exist in existing installations if (!fs.existsSync(this.cacheDir)) fs.mkdirSync(this.cacheDir); } + #tempfilePath() { + const filename = `${v4()}.tmp`; + const tmpPath = process.env.STORAGE_DIR + ? path.resolve(process.env.STORAGE_DIR, "tmp") + : path.resolve(__dirname, `../../../storage/tmp`); + if (!fs.existsSync(tmpPath)) fs.mkdirSync(tmpPath, { recursive: true }); + return path.resolve(tmpPath, filename); + } + + async #writeToTempfile(filePath, data) { + try { + await fs.promises.appendFile(filePath, data, { encoding: "utf8" }); + } catch (e) { + console.error(`Error writing to tempfile: ${e}`); + } + } + async embedderClient() { if (!fs.existsSync(this.modelPath)) { console.log( @@ -61,18 +79,51 @@ class NativeEmbedder { return result?.[0] || []; } + // If you are thinking you want to edit this function - you probably don't. + // This process was benchmarked heavily on a t3.small (2GB RAM 1vCPU) + // and without careful memory management for the V8 garbage collector + // this function will likely result in an OOM on any resource-constrained deployment. + // To help manage very large documents we run a concurrent write-log each iteration + // to keep the embedding result out of memory. The `maxConcurrentChunk` is set to 25, + // as 50 seems to overflow no matter what. Given the above, memory use hovers around ~30% + // during a very large document (>100K words) but can spike up to 70% before gc. + // This seems repeatable for all document sizes. + // While this does take a while, it is zero set up and is 100% free and on-instance. async embedChunks(textChunks = []) { - const Embedder = await this.embedderClient(); - const embeddingResults = []; - for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) { - const output = await Embedder(chunk, { + const tmpFilePath = this.#tempfilePath(); + const chunks = toChunks(textChunks, this.maxConcurrentChunks); + const chunkLen = chunks.length; + + for (let [idx, chunk] of chunks.entries()) { + if (idx === 0) await this.#writeToTempfile(tmpFilePath, "["); + let data; + let pipeline = await this.embedderClient(); + let output = await pipeline(chunk, { pooling: "mean", normalize: true, }); - if (output.length === 0) continue; - embeddingResults.push(output.tolist()); + + if (output.length === 0) { + pipeline = null; + output = null; + data = null; + continue; + } + + data = JSON.stringify(output.tolist()); + await this.#writeToTempfile(tmpFilePath, data); + console.log(`\x1b[34m[Embedded Chunk ${idx + 1} of ${chunkLen}]\x1b[0m`); + if (chunkLen - 1 !== idx) await this.#writeToTempfile(tmpFilePath, ","); + if (chunkLen - 1 === idx) await this.#writeToTempfile(tmpFilePath, "]"); + pipeline = null; + output = null; + data = null; } + const embeddingResults = JSON.parse( + fs.readFileSync(tmpFilePath, { encoding: "utf-8" }) + ); + fs.rmSync(tmpFilePath, { force: true }); return embeddingResults.length > 0 ? embeddingResults.flat() : null; } } diff --git a/server/utils/chats/index.js b/server/utils/chats/index.js index 7e9be6e5b..764c7795a 100644 --- a/server/utils/chats/index.js +++ b/server/utils/chats/index.js @@ -71,7 +71,7 @@ async function chatWithWorkspace( return await VALID_COMMANDS[command](workspace, message, uuid, user); } - const LLMConnector = getLLMProvider(); + const LLMConnector = getLLMProvider(workspace?.chatModel); const VectorDb = getVectorDbClass(); const { safe, reasons = [] } = await LLMConnector.isSafe(message); if (!safe) { @@ -91,6 +91,18 @@ async function chatWithWorkspace( const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug); const embeddingsCount = await VectorDb.namespaceCount(workspace.slug); if (!hasVectorizedSpace || embeddingsCount === 0) { + if (chatMode === "query") { + return { + id: uuid, + type: "textResponse", + sources: [], + close: true, + error: null, + textResponse: + "There is no relevant information in this workspace to answer your query.", + }; + } + // If there are no embeddings - chat like a normal LLM chat interface. return await emptyEmbeddingChat({ uuid, @@ -131,6 +143,20 @@ async function chatWithWorkspace( }; } + // If in query mode and no sources are found, do not + // let the LLM try to hallucinate a response or use general knowledge + if (chatMode === "query" && sources.length === 0) { + return { + id: uuid, + type: "textResponse", + sources: [], + close: true, + error: null, + textResponse: + "There is no relevant information in this workspace to answer your query.", + }; + } + // Compress message to ensure prompt passes token limit with room for response // and build system messages based on inputs and history. const messages = await LLMConnector.compressMessages( @@ -145,7 +171,7 @@ async function chatWithWorkspace( // Send the text completion. const textResponse = await LLMConnector.getChatCompletion(messages, { - temperature: workspace?.openAiTemp ?? 0.7, + temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp, }); if (!textResponse) { diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index a6ade1819..cff565ed6 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -8,6 +8,7 @@ const { chatPrompt, } = require("."); +const VALID_CHAT_MODE = ["chat", "query"]; function writeResponseChunk(response, data) { response.write(`data: ${JSON.stringify(data)}\n\n`); return; @@ -29,7 +30,7 @@ async function streamChatWithWorkspace( return; } - const LLMConnector = getLLMProvider(); + const LLMConnector = getLLMProvider(workspace?.chatModel); const VectorDb = getVectorDbClass(); const { safe, reasons = [] } = await LLMConnector.isSafe(message); if (!safe) { @@ -50,6 +51,19 @@ async function streamChatWithWorkspace( const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug); const embeddingsCount = await VectorDb.namespaceCount(workspace.slug); if (!hasVectorizedSpace || embeddingsCount === 0) { + if (chatMode === "query") { + writeResponseChunk(response, { + id: uuid, + type: "textResponse", + textResponse: + "There is no relevant information in this workspace to answer your query.", + sources: [], + close: true, + error: null, + }); + return; + } + // If there are no embeddings - chat like a normal LLM chat interface. return await streamEmptyEmbeddingChat({ response, @@ -93,6 +107,21 @@ async function streamChatWithWorkspace( return; } + // If in query mode and no sources are found, do not + // let the LLM try to hallucinate a response or use general knowledge + if (chatMode === "query" && sources.length === 0) { + writeResponseChunk(response, { + id: uuid, + type: "textResponse", + textResponse: + "There is no relevant information in this workspace to answer your query.", + sources: [], + close: true, + error: null, + }); + return; + } + // Compress message to ensure prompt passes token limit with room for response // and build system messages based on inputs and history. const messages = await LLMConnector.compressMessages( @@ -112,7 +141,7 @@ async function streamChatWithWorkspace( `\x1b[31m[STREAMING DISABLED]\x1b[0m Streaming is not available for ${LLMConnector.constructor.name}. Will use regular chat method.` ); completeText = await LLMConnector.getChatCompletion(messages, { - temperature: workspace?.openAiTemp ?? 0.7, + temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp, }); writeResponseChunk(response, { uuid, @@ -124,7 +153,7 @@ async function streamChatWithWorkspace( }); } else { const stream = await LLMConnector.streamGetChatCompletion(messages, { - temperature: workspace?.openAiTemp ?? 0.7, + temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp, }); completeText = await handleStreamResponses(response, stream, { uuid, @@ -262,6 +291,96 @@ function handleStreamResponses(response, stream, responseProps) { }); } + if (stream.type === "togetherAiStream") { + return new Promise((resolve) => { + let fullText = ""; + let chunk = ""; + stream.stream.data.on("data", (data) => { + const lines = data + ?.toString() + ?.split("\n") + .filter((line) => line.trim() !== ""); + + for (const line of lines) { + let validJSON = false; + const message = chunk + line.replace(/^data: /, ""); + + if (message !== "[DONE]") { + // JSON chunk is incomplete and has not ended yet + // so we need to stitch it together. You would think JSON + // chunks would only come complete - but they don't! + try { + JSON.parse(message); + validJSON = true; + } catch {} + + if (!validJSON) { + // It can be possible that the chunk decoding is running away + // and the message chunk fails to append due to string length. + // In this case abort the chunk and reset so we can continue. + // ref: https://github.com/Mintplex-Labs/anything-llm/issues/416 + try { + chunk += message; + } catch (e) { + console.error(`Chunk appending error`, e); + chunk = ""; + } + continue; + } else { + chunk = ""; + } + } + + if (message == "[DONE]") { + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + resolve(fullText); + } else { + let finishReason = null; + let token = ""; + try { + const json = JSON.parse(message); + token = json?.choices?.[0]?.delta?.content; + finishReason = json?.choices?.[0]?.finish_reason || null; + } catch { + continue; + } + + if (token) { + fullText += token; + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: token, + close: false, + error: false, + }); + } + + if (finishReason !== null) { + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + resolve(fullText); + } + } + } + }); + }); + } + // If stream is not a regular OpenAI Stream (like if using native model, Ollama, or most LangChain interfaces) // we can just iterate the stream content instead. if (!stream.hasOwnProperty("data")) { @@ -385,6 +504,7 @@ function handleStreamResponses(response, stream, responseProps) { } module.exports = { + VALID_CHAT_MODE, streamChatWithWorkspace, writeResponseChunk, }; diff --git a/server/utils/files/documentProcessor.js b/server/utils/files/documentProcessor.js index 5239a8708..27d0f5f2b 100644 --- a/server/utils/files/documentProcessor.js +++ b/server/utils/files/documentProcessor.js @@ -35,7 +35,7 @@ async function processDocument(filename = "") { .then((res) => res) .catch((e) => { console.log(e.message); - return { success: false, reason: e.message }; + return { success: false, reason: e.message, documents: [] }; }); } @@ -55,7 +55,7 @@ async function processLink(link = "") { .then((res) => res) .catch((e) => { console.log(e.message); - return { success: false, reason: e.message }; + return { success: false, reason: e.message, documents: [] }; }); } diff --git a/server/utils/files/index.js b/server/utils/files/index.js index b6c7a3070..e713a318a 100644 --- a/server/utils/files/index.js +++ b/server/utils/files/index.js @@ -2,32 +2,6 @@ const fs = require("fs"); const path = require("path"); const { v5: uuidv5 } = require("uuid"); -async function collectDocumentData(folderName = null) { - if (!folderName) throw new Error("No docPath provided in request"); - const folder = - process.env.NODE_ENV === "development" - ? path.resolve(__dirname, `../../storage/documents/${folderName}`) - : path.resolve(process.env.STORAGE_DIR, `documents/${folderName}`); - - const dirExists = fs.existsSync(folder); - if (!dirExists) - throw new Error( - `No documents folder for ${folderName} - did you run collector/main.py for this element?` - ); - - const files = fs.readdirSync(folder); - const fileData = []; - files.forEach((file) => { - if (path.extname(file) === ".json") { - const filePath = path.join(folder, file); - const data = fs.readFileSync(filePath, "utf8"); - console.log(`Parsing document: ${file}`); - fileData.push(JSON.parse(data)); - } - }); - return fileData; -} - // Should take in a folder that is a subfolder of documents // eg: youtube-subject/video-123.json async function fileData(filePath = null) { @@ -35,8 +9,15 @@ async function fileData(filePath = null) { const fullPath = process.env.NODE_ENV === "development" - ? path.resolve(__dirname, `../../storage/documents/${filePath}`) - : path.resolve(process.env.STORAGE_DIR, `documents/${filePath}`); + ? path.resolve( + __dirname, + `../../storage/documents/${normalizePath(filePath)}` + ) + : path.resolve( + process.env.STORAGE_DIR, + `documents/${normalizePath(filePath)}` + ); + const fileExists = fs.existsSync(fullPath); if (!fileExists) return null; @@ -142,11 +123,18 @@ async function storeVectorResult(vectorData = [], filename = null) { async function purgeSourceDocument(filename = null) { if (!filename) return; console.log(`Purging source document of ${filename}.`); - const filePath = process.env.NODE_ENV === "development" - ? path.resolve(__dirname, `../../storage/documents`, filename) - : path.resolve(process.env.STORAGE_DIR, `documents`, filename); + ? path.resolve( + __dirname, + `../../storage/documents`, + normalizePath(filename) + ) + : path.resolve( + process.env.STORAGE_DIR, + `documents`, + normalizePath(filename) + ); if (!fs.existsSync(filePath)) return; fs.rmSync(filePath); @@ -169,12 +157,54 @@ async function purgeVectorCache(filename = null) { return; } +// Search for a specific document by its unique name in the entire `documents` +// folder via iteration of all folders and checking if the expected file exists. +async function findDocumentInDocuments(documentName = null) { + if (!documentName) return null; + const documentsFolder = + process.env.NODE_ENV === "development" + ? path.resolve(__dirname, `../../storage/documents`) + : path.resolve(process.env.STORAGE_DIR, `documents`); + + for (const folder of fs.readdirSync(documentsFolder)) { + const isFolder = fs + .lstatSync(path.join(documentsFolder, folder)) + .isDirectory(); + if (!isFolder) continue; + + const targetFilename = normalizePath(documentName); + const targetFileLocation = path.join( + documentsFolder, + folder, + targetFilename + ); + if (!fs.existsSync(targetFileLocation)) continue; + + const fileData = fs.readFileSync(targetFileLocation, "utf8"); + const cachefilename = `${folder}/${targetFilename}`; + const { pageContent, ...metadata } = JSON.parse(fileData); + return { + name: targetFilename, + type: "file", + ...metadata, + cached: await cachedVectorInformation(cachefilename, true), + }; + } + + return null; +} + +function normalizePath(filepath = "") { + return path.normalize(filepath).replace(/^(\.\.(\/|\\|$))+/, ""); +} + module.exports = { + findDocumentInDocuments, cachedVectorInformation, - collectDocumentData, viewLocalFiles, purgeSourceDocument, purgeVectorCache, storeVectorResult, fileData, + normalizePath, }; diff --git a/server/utils/files/pfp.js b/server/utils/files/pfp.js index 943aa595f..dd6ba0fe2 100644 --- a/server/utils/files/pfp.js +++ b/server/utils/files/pfp.js @@ -2,6 +2,7 @@ const path = require("path"); const fs = require("fs"); const { getType } = require("mime"); const { User } = require("../../models/user"); +const { normalizePath } = require("."); function fetchPfp(pfpPath) { if (!fs.existsSync(pfpPath)) { @@ -32,8 +33,7 @@ async function determinePfpFilepath(id) { const basePath = process.env.STORAGE_DIR ? path.join(process.env.STORAGE_DIR, "assets/pfp") : path.join(__dirname, "../../storage/assets/pfp"); - const pfpFilepath = path.join(basePath, pfpFilename); - + const pfpFilepath = path.join(basePath, normalizePath(pfpFilename)); if (!fs.existsSync(pfpFilepath)) return null; return pfpFilepath; } diff --git a/server/utils/files/purgeDocument.js b/server/utils/files/purgeDocument.js index 27fe14710..46e9d37da 100644 --- a/server/utils/files/purgeDocument.js +++ b/server/utils/files/purgeDocument.js @@ -1,7 +1,6 @@ const fs = require("fs"); const path = require("path"); - -const { purgeVectorCache, purgeSourceDocument } = require("."); +const { purgeVectorCache, purgeSourceDocument, normalizePath } = require("."); const { Document } = require("../../models/documents"); const { Workspace } = require("../../models/workspace"); @@ -22,10 +21,10 @@ async function purgeFolder(folderName) { ? path.resolve(__dirname, `../../storage/documents`) : path.resolve(process.env.STORAGE_DIR, `documents`); - const folderPath = path.resolve(documentsFolder, folderName); + const folderPath = path.resolve(documentsFolder, normalizePath(folderName)); const filenames = fs .readdirSync(folderPath) - .map((file) => path.join(folderName, file)); + .map((file) => path.join(folderPath, file)); const workspaces = await Workspace.where(); const purgePromises = []; diff --git a/server/utils/helpers/customModels.js b/server/utils/helpers/customModels.js index 5bd7b299e..53c641e75 100644 --- a/server/utils/helpers/customModels.js +++ b/server/utils/helpers/customModels.js @@ -1,4 +1,12 @@ -const SUPPORT_CUSTOM_MODELS = ["openai", "localai", "ollama", "native-llm"]; +const { togetherAiModels } = require("../AiProviders/togetherAi"); +const SUPPORT_CUSTOM_MODELS = [ + "openai", + "localai", + "ollama", + "native-llm", + "togetherai", + "mistral", +]; async function getCustomModels(provider = "", apiKey = null, basePath = null) { if (!SUPPORT_CUSTOM_MODELS.includes(provider)) @@ -10,7 +18,11 @@ async function getCustomModels(provider = "", apiKey = null, basePath = null) { case "localai": return await localAIModels(basePath, apiKey); case "ollama": - return await ollamaAIModels(basePath, apiKey); + return await ollamaAIModels(basePath); + case "togetherai": + return await getTogetherAiModels(); + case "mistral": + return await getMistralModels(apiKey); case "native-llm": return nativeLLMModels(); default: @@ -44,7 +56,7 @@ async function openAiModels(apiKey = null) { async function localAIModels(basePath = null, apiKey = null) { const { Configuration, OpenAIApi } = require("openai"); const config = new Configuration({ - basePath, + basePath: basePath || process.env.LOCAL_AI_BASE_PATH, apiKey: apiKey || process.env.LOCAL_AI_API_KEY, }); const openai = new OpenAIApi(config); @@ -61,13 +73,14 @@ async function localAIModels(basePath = null, apiKey = null) { return { models, error: null }; } -async function ollamaAIModels(basePath = null, _apiKey = null) { +async function ollamaAIModels(basePath = null) { let url; try { - new URL(basePath); - if (basePath.split("").slice(-1)?.[0] === "/") + let urlPath = basePath ?? process.env.OLLAMA_BASE_PATH; + new URL(urlPath); + if (urlPath.split("").slice(-1)?.[0] === "/") throw new Error("BasePath Cannot end in /!"); - url = basePath; + url = urlPath; } catch { return { models: [], error: "Not a valid URL." }; } @@ -92,6 +105,41 @@ async function ollamaAIModels(basePath = null, _apiKey = null) { return { models, error: null }; } +async function getTogetherAiModels() { + const knownModels = togetherAiModels(); + if (!Object.keys(knownModels).length === 0) + return { models: [], error: null }; + + const models = Object.values(knownModels).map((model) => { + return { + id: model.id, + organization: model.organization, + name: model.name, + }; + }); + return { models, error: null }; +} + +async function getMistralModels(apiKey = null) { + const { Configuration, OpenAIApi } = require("openai"); + const config = new Configuration({ + apiKey: apiKey || process.env.MISTRAL_API_KEY, + basePath: "https://api.mistral.ai/v1", + }); + const openai = new OpenAIApi(config); + const models = await openai + .listModels() + .then((res) => res.data.data.filter((model) => !model.id.includes("embed"))) + .catch((e) => { + console.error(`Mistral:listModels`, e.message); + return []; + }); + + // Api Key was successful so lets save it for future uses + if (models.length > 0 && !!apiKey) process.env.MISTRAL_API_KEY = apiKey; + return { models, error: null }; +} + function nativeLLMModels() { const fs = require("fs"); const path = require("path"); diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index bde5e8a0a..b72bb7977 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -16,39 +16,51 @@ function getVectorDbClass() { case "qdrant": const { QDrant } = require("../vectorDbProviders/qdrant"); return QDrant; + case "milvus": + const { Milvus } = require("../vectorDbProviders/milvus"); + return Milvus; + case "zilliz": + const { Zilliz } = require("../vectorDbProviders/zilliz"); + return Zilliz; default: throw new Error("ENV: No VECTOR_DB value found in environment!"); } } -function getLLMProvider() { +function getLLMProvider(modelPreference = null) { const vectorSelection = process.env.LLM_PROVIDER || "openai"; const embedder = getEmbeddingEngineSelection(); switch (vectorSelection) { case "openai": const { OpenAiLLM } = require("../AiProviders/openAi"); - return new OpenAiLLM(embedder); + return new OpenAiLLM(embedder, modelPreference); case "azure": const { AzureOpenAiLLM } = require("../AiProviders/azureOpenAi"); - return new AzureOpenAiLLM(embedder); + return new AzureOpenAiLLM(embedder, modelPreference); case "anthropic": const { AnthropicLLM } = require("../AiProviders/anthropic"); - return new AnthropicLLM(embedder); + return new AnthropicLLM(embedder, modelPreference); case "gemini": const { GeminiLLM } = require("../AiProviders/gemini"); - return new GeminiLLM(embedder); + return new GeminiLLM(embedder, modelPreference); case "lmstudio": const { LMStudioLLM } = require("../AiProviders/lmStudio"); - return new LMStudioLLM(embedder); + return new LMStudioLLM(embedder, modelPreference); case "localai": const { LocalAiLLM } = require("../AiProviders/localAi"); - return new LocalAiLLM(embedder); + return new LocalAiLLM(embedder, modelPreference); case "ollama": const { OllamaAILLM } = require("../AiProviders/ollama"); - return new OllamaAILLM(embedder); + return new OllamaAILLM(embedder, modelPreference); + case "togetherai": + const { TogetherAiLLM } = require("../AiProviders/togetherAi"); + return new TogetherAiLLM(embedder, modelPreference); + case "mistral": + const { MistralLLM } = require("../AiProviders/mistral"); + return new MistralLLM(embedder, modelPreference); case "native": const { NativeLLM } = require("../AiProviders/native"); - return new NativeLLM(embedder); + return new NativeLLM(embedder, modelPreference); default: throw new Error("ENV: No LLM_PROVIDER value found in environment!"); } @@ -70,6 +82,7 @@ function getEmbeddingEngineSelection() { return new LocalAiEmbedder(); case "native": const { NativeEmbedder } = require("../EmbeddingEngines/native"); + console.log("\x1b[34m[INFO]\x1b[0m Using Native Embedder"); return new NativeEmbedder(); default: return null; diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index ebaadba73..45f006f6f 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -2,6 +2,7 @@ const KEY_MAPPING = { LLMProvider: { envKey: "LLM_PROVIDER", checks: [isNotEmpty, supportedLLM], + postUpdate: [wipeWorkspaceModelPreference], }, // OpenAI Settings OpenAiKey: { @@ -94,12 +95,26 @@ const KEY_MAPPING = { checks: [nonZero], }, + MistralApiKey: { + envKey: "MISTRAL_API_KEY", + checks: [isNotEmpty], + }, + MistralModelPref: { + envKey: "MISTRAL_MODEL_PREF", + checks: [isNotEmpty], + }, + // Native LLM Settings NativeLLMModelPref: { envKey: "NATIVE_LLM_MODEL_PREF", checks: [isDownloadedModel], }, + NativeLLMTokenLimit: { + envKey: "NATIVE_LLM_MODEL_TOKEN_LIMIT", + checks: [nonZero], + }, + EmbeddingEngine: { envKey: "EMBEDDING_ENGINE", checks: [supportedEmbeddingModel], @@ -170,6 +185,40 @@ const KEY_MAPPING = { checks: [], }, + // Milvus Options + MilvusAddress: { + envKey: "MILVUS_ADDRESS", + checks: [isValidURL, validDockerizedUrl], + }, + MilvusUsername: { + envKey: "MILVUS_USERNAME", + checks: [isNotEmpty], + }, + MilvusPassword: { + envKey: "MILVUS_PASSWORD", + checks: [isNotEmpty], + }, + + // Zilliz Cloud Options + ZillizEndpoint: { + envKey: "ZILLIZ_ENDPOINT", + checks: [isValidURL], + }, + ZillizApiToken: { + envKey: "ZILLIZ_API_TOKEN", + checks: [isNotEmpty], + }, + + // Together Ai Options + TogetherAiApiKey: { + envKey: "TOGETHER_AI_API_KEY", + checks: [isNotEmpty], + }, + TogetherAiModelPref: { + envKey: "TOGETHER_AI_MODEL_PREF", + checks: [isNotEmpty], + }, + // System Settings AuthToken: { envKey: "AUTH_TOKEN", @@ -233,7 +282,7 @@ function validOllamaLLMBasePath(input = "") { } function supportedLLM(input = "") { - return [ + const validSelection = [ "openai", "azure", "anthropic", @@ -242,7 +291,10 @@ function supportedLLM(input = "") { "localai", "ollama", "native", + "togetherai", + "mistral", ].includes(input); + return validSelection ? null : `${input} is not a valid LLM provider.`; } function validGeminiModel(input = "") { @@ -267,7 +319,15 @@ function supportedEmbeddingModel(input = "") { } function supportedVectorDB(input = "") { - const supported = ["chroma", "pinecone", "lancedb", "weaviate", "qdrant"]; + const supported = [ + "chroma", + "pinecone", + "lancedb", + "weaviate", + "qdrant", + "milvus", + "zilliz", + ]; return supported.includes(input) ? null : `Invalid VectorDB type. Must be one of ${supported.join(", ")}.`; @@ -329,11 +389,20 @@ function validDockerizedUrl(input = "") { return null; } +// If the LLMProvider has changed we need to reset all workspace model preferences to +// null since the provider<>model name combination will be invalid for whatever the new +// provider is. +async function wipeWorkspaceModelPreference(key, prev, next) { + if (prev === next) return; + const { Workspace } = require("../../models/workspace"); + await Workspace.resetWorkspaceChatModels(); +} + // This will force update .env variables which for any which reason were not able to be parsed or // read from an ENV file as this seems to be a complicating step for many so allowing people to write // to the process will at least alleviate that issue. It does not perform comprehensive validity checks or sanity checks // and is simply for debugging when the .env not found issue many come across. -function updateENV(newENVs = {}, force = false) { +async function updateENV(newENVs = {}, force = false) { let error = ""; const validKeys = Object.keys(KEY_MAPPING); const ENV_KEYS = Object.keys(newENVs).filter( @@ -341,21 +410,25 @@ function updateENV(newENVs = {}, force = false) { ); const newValues = {}; - ENV_KEYS.forEach((key) => { - const { envKey, checks } = KEY_MAPPING[key]; - const value = newENVs[key]; + for (const key of ENV_KEYS) { + const { envKey, checks, postUpdate = [] } = KEY_MAPPING[key]; + const prevValue = process.env[envKey]; + const nextValue = newENVs[key]; const errors = checks - .map((validityCheck) => validityCheck(value, force)) + .map((validityCheck) => validityCheck(nextValue, force)) .filter((err) => typeof err === "string"); if (errors.length > 0) { error += errors.join("\n"); - return; + break; } - newValues[key] = value; - process.env[envKey] = value; - }); + newValues[key] = nextValue; + process.env[envKey] = nextValue; + + for (const postUpdateFunc of postUpdate) + await postUpdateFunc(key, prevValue, nextValue); + } return { newValues, error: error?.length > 0 ? error : false }; } diff --git a/server/utils/http/index.js b/server/utils/http/index.js index d9d9d0485..d4551dae6 100644 --- a/server/utils/http/index.js +++ b/server/utils/http/index.js @@ -25,6 +25,8 @@ function makeJWT(info = {}, expiry = "30d") { return JWT.sign(info, process.env.JWT_SECRET, { expiresIn: expiry }); } +// Note: Only valid for finding users in multi-user mode +// as single-user mode with password is not a "user" async function userFromSession(request, response = null) { if (!!response && !!response.locals?.user) { return response.locals.user; diff --git a/server/utils/middleware/validatedRequest.js b/server/utils/middleware/validatedRequest.js index 275522bb9..6f3df26da 100644 --- a/server/utils/middleware/validatedRequest.js +++ b/server/utils/middleware/validatedRequest.js @@ -36,8 +36,9 @@ async function validatedRequest(request, response, next) { return; } + const bcrypt = require("bcrypt"); const { p } = decodeJWT(token); - if (p !== process.env.AUTH_TOKEN) { + if (!bcrypt.compareSync(p, bcrypt.hashSync(process.env.AUTH_TOKEN, 10))) { response.status(401).json({ error: "Invalid auth token found.", }); diff --git a/server/utils/vectorDbProviders/milvus/MILVUS_SETUP.md b/server/utils/vectorDbProviders/milvus/MILVUS_SETUP.md new file mode 100644 index 000000000..6bd9b8150 --- /dev/null +++ b/server/utils/vectorDbProviders/milvus/MILVUS_SETUP.md @@ -0,0 +1,40 @@ +# How to setup a local (or remote) Milvus Vector Database + +[Official Milvus Docs](https://milvus.io/docs/example_code.md) for reference. + +### How to get started + +**Requirements** + +Choose one of the following + +- Cloud + + - [Cloud account](https://cloud.zilliz.com/) + +- Local + - Docker + - `git` available in your CLI/terminal + +**Instructions** + +- Cloud + + - Create a Cluster on your cloud account + - Get connect Public Endpoint and Token + - Set .env.development variable in server + +- Local + - Download yaml file `wget https://github.com/milvus-io/milvus/releases/download/v2.3.4/milvus-standalone-docker-compose.yml -O docker-compose.yml` + - Start Milvus `sudo docker compose up -d` + - Check the containers are up and running `sudo docker compose ps` + - Get port number and set .env.development variable in server + +eg: `server/.env.development` + +``` +VECTOR_DB="milvus" +MILVUS_ADDRESS="http://localhost:19530" +MILVUS_USERNAME=minioadmin # Whatever your username and password are +MILVUS_PASSWORD=minioadmin +``` diff --git a/server/utils/vectorDbProviders/milvus/index.js b/server/utils/vectorDbProviders/milvus/index.js new file mode 100644 index 000000000..cc934a9a2 --- /dev/null +++ b/server/utils/vectorDbProviders/milvus/index.js @@ -0,0 +1,364 @@ +const { + DataType, + MetricType, + IndexType, + MilvusClient, +} = require("@zilliz/milvus2-sdk-node"); +const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); +const { v4: uuidv4 } = require("uuid"); +const { storeVectorResult, cachedVectorInformation } = require("../../files"); +const { + toChunks, + getLLMProvider, + getEmbeddingEngineSelection, +} = require("../../helpers"); + +const Milvus = { + name: "Milvus", + connect: async function () { + if (process.env.VECTOR_DB !== "milvus") + throw new Error("Milvus::Invalid ENV settings"); + + const client = new MilvusClient({ + address: process.env.MILVUS_ADDRESS, + username: process.env.MILVUS_USERNAME, + password: process.env.MILVUS_PASSWORD, + }); + + const { isHealthy } = await client.checkHealth(); + if (!isHealthy) + throw new Error( + "MilvusDB::Invalid Heartbeat received - is the instance online?" + ); + + return { client }; + }, + heartbeat: async function () { + await this.connect(); + return { heartbeat: Number(new Date()) }; + }, + totalVectors: async function () { + const { client } = await this.connect(); + const { collection_names } = await client.listCollections(); + const total = collection_names.reduce(async (acc, collection_name) => { + const statistics = await client.getCollectionStatistics({ + collection_name, + }); + return Number(acc) + Number(statistics?.data?.row_count ?? 0); + }, 0); + return total; + }, + namespaceCount: async function (_namespace = null) { + const { client } = await this.connect(); + const statistics = await client.getCollectionStatistics({ + collection_name: _namespace, + }); + return Number(statistics?.data?.row_count ?? 0); + }, + namespace: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const collection = await client + .getCollectionStatistics({ collection_name: namespace }) + .catch(() => null); + return collection; + }, + hasNamespace: async function (namespace = null) { + if (!namespace) return false; + const { client } = await this.connect(); + return await this.namespaceExists(client, namespace); + }, + namespaceExists: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const { value } = await client + .hasCollection({ collection_name: namespace }) + .catch((e) => { + console.error("MilvusDB::namespaceExists", e.message); + return { value: false }; + }); + return value; + }, + deleteVectorsInNamespace: async function (client, namespace = null) { + await client.dropCollection({ collection_name: namespace }); + return true; + }, + // Milvus requires a dimension aspect for collection creation + // we pass this in from the first chunk to infer the dimensions like other + // providers do. + getOrCreateCollection: async function (client, namespace, dimensions = null) { + const isExists = await this.namespaceExists(client, namespace); + if (!isExists) { + if (!dimensions) + throw new Error( + `Milvus:getOrCreateCollection Unable to infer vector dimension from input. Open an issue on Github for support.` + ); + + await client.createCollection({ + collection_name: namespace, + fields: [ + { + name: "id", + description: "id", + data_type: DataType.VarChar, + max_length: 255, + is_primary_key: true, + }, + { + name: "vector", + description: "vector", + data_type: DataType.FloatVector, + dim: dimensions, + }, + { + name: "metadata", + decription: "metadata", + data_type: DataType.JSON, + }, + ], + }); + await client.createIndex({ + collection_name: namespace, + field_name: "vector", + index_type: IndexType.AUTOINDEX, + metric_type: MetricType.COSINE, + }); + await client.loadCollectionSync({ + collection_name: namespace, + }); + } + }, + addDocumentToNamespace: async function ( + namespace, + documentData = {}, + fullFilePath = null + ) { + const { DocumentVectors } = require("../../../models/vectors"); + try { + let vectorDimension = null; + const { pageContent, docId, ...metadata } = documentData; + if (!pageContent || pageContent.length == 0) return false; + + console.log("Adding new vectorized document into namespace", namespace); + const cacheResult = await cachedVectorInformation(fullFilePath); + if (cacheResult.exists) { + const { client } = await this.connect(); + const { chunks } = cacheResult; + const documentVectors = []; + vectorDimension = chunks[0][0].values.length || null; + + await this.getOrCreateCollection(client, namespace, vectorDimension); + for (const chunk of chunks) { + // Before sending to Pinecone and saving the records to our db + // we need to assign the id of each chunk that is stored in the cached file. + const newChunks = chunk.map((chunk) => { + const id = uuidv4(); + documentVectors.push({ docId, vectorId: id }); + return { id, vector: chunk.values, metadata: chunk.metadata }; + }); + const insertResult = await client.insert({ + collection_name: namespace, + data: newChunks, + }); + + if (insertResult?.status.error_code !== "Success") { + throw new Error( + `Error embedding into Milvus! Reason:${insertResult?.status.reason}` + ); + } + } + await DocumentVectors.bulkInsert(documentVectors); + await client.flushSync({ collection_names: [namespace] }); + return true; + } + + const textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize: + getEmbeddingEngineSelection()?.embeddingMaxChunkLength || 1_000, + chunkOverlap: 20, + }); + const textChunks = await textSplitter.splitText(pageContent); + + console.log("Chunks created from document:", textChunks.length); + const LLMConnector = getLLMProvider(); + const documentVectors = []; + const vectors = []; + const vectorValues = await LLMConnector.embedChunks(textChunks); + + if (!!vectorValues && vectorValues.length > 0) { + for (const [i, vector] of vectorValues.entries()) { + if (!vectorDimension) vectorDimension = vector.length; + const vectorRecord = { + id: uuidv4(), + values: vector, + // [DO NOT REMOVE] + // LangChain will be unable to find your text if you embed manually and dont include the `text` key. + metadata: { ...metadata, text: textChunks[i] }, + }; + + vectors.push(vectorRecord); + documentVectors.push({ docId, vectorId: vectorRecord.id }); + } + } else { + throw new Error( + "Could not embed document chunks! This document will not be recorded." + ); + } + + if (vectors.length > 0) { + const chunks = []; + const { client } = await this.connect(); + await this.getOrCreateCollection(client, namespace, vectorDimension); + + console.log("Inserting vectorized chunks into Milvus."); + for (const chunk of toChunks(vectors, 100)) { + chunks.push(chunk); + const insertResult = await client.insert({ + collection_name: namespace, + data: chunk.map((item) => ({ + id: item.id, + vector: item.values, + metadata: chunk.metadata, + })), + }); + + if (insertResult?.status.error_code !== "Success") { + throw new Error( + `Error embedding into Milvus! Reason:${insertResult?.status.reason}` + ); + } + } + await storeVectorResult(chunks, fullFilePath); + await client.flushSync({ collection_names: [namespace] }); + } + + await DocumentVectors.bulkInsert(documentVectors); + return true; + } catch (e) { + console.error(e); + console.error("addDocumentToNamespace", e.message); + return false; + } + }, + deleteDocumentFromNamespace: async function (namespace, docId) { + const { DocumentVectors } = require("../../../models/vectors"); + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) return; + const knownDocuments = await DocumentVectors.where({ docId }); + if (knownDocuments.length === 0) return; + + const vectorIds = knownDocuments.map((doc) => doc.vectorId); + const queryIn = vectorIds.map((v) => `'${v}'`).join(","); + await client.deleteEntities({ + collection_name: namespace, + expr: `id in [${queryIn}]`, + }); + + const indexes = knownDocuments.map((doc) => doc.id); + await DocumentVectors.deleteIds(indexes); + + // Even after flushing Milvus can take some time to re-calc the count + // so all we can hope to do is flushSync so that the count can be correct + // on a later call. + await client.flushSync({ collection_names: [namespace] }); + return true; + }, + performSimilaritySearch: async function ({ + namespace = null, + input = "", + LLMConnector = null, + similarityThreshold = 0.25, + }) { + if (!namespace || !input || !LLMConnector) + throw new Error("Invalid request to performSimilaritySearch."); + + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) { + return { + contextTexts: [], + sources: [], + message: "Invalid query - no documents found for workspace!", + }; + } + + const queryVector = await LLMConnector.embedTextInput(input); + const { contextTexts, sourceDocuments } = await this.similarityResponse( + client, + namespace, + queryVector, + similarityThreshold + ); + + const sources = sourceDocuments.map((metadata, i) => { + return { ...metadata, text: contextTexts[i] }; + }); + return { + contextTexts, + sources: this.curateSources(sources), + message: false, + }; + }, + similarityResponse: async function ( + client, + namespace, + queryVector, + similarityThreshold = 0.25 + ) { + const result = { + contextTexts: [], + sourceDocuments: [], + scores: [], + }; + const response = await client.search({ + collection_name: namespace, + vectors: queryVector, + }); + response.results.forEach((match) => { + if (match.score < similarityThreshold) return; + result.contextTexts.push(match.metadata.text); + result.sourceDocuments.push(match); + result.scores.push(match.score); + }); + return result; + }, + "namespace-stats": async function (reqBody = {}) { + const { namespace = null } = reqBody; + if (!namespace) throw new Error("namespace required"); + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error("Namespace by that name does not exist."); + const stats = await this.namespace(client, namespace); + return stats + ? stats + : { message: "No stats were able to be fetched from DB for namespace" }; + }, + "delete-namespace": async function (reqBody = {}) { + const { namespace = null } = reqBody; + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error("Namespace by that name does not exist."); + + const statistics = await this.namespace(client, namespace); + await this.deleteVectorsInNamespace(client, namespace); + const vectorCount = Number(statistics?.data?.row_count ?? 0); + return { + message: `Namespace ${namespace} was deleted along with ${vectorCount} vectors.`, + }; + }, + curateSources: function (sources = []) { + const documents = []; + for (const source of sources) { + const { metadata = {} } = source; + if (Object.keys(metadata).length > 0) { + documents.push({ + ...metadata, + ...(source.hasOwnProperty("pageContent") + ? { text: source.pageContent } + : {}), + }); + } + } + + return documents; + }, +}; + +module.exports.Milvus = Milvus; diff --git a/server/utils/vectorDbProviders/qdrant/index.js b/server/utils/vectorDbProviders/qdrant/index.js index 49b25a3d6..2783cde93 100644 --- a/server/utils/vectorDbProviders/qdrant/index.js +++ b/server/utils/vectorDbProviders/qdrant/index.js @@ -108,13 +108,20 @@ const QDrant = { await client.deleteCollection(namespace); return true; }, - getOrCreateCollection: async function (client, namespace) { + // QDrant requires a dimension aspect for collection creation + // we pass this in from the first chunk to infer the dimensions like other + // providers do. + getOrCreateCollection: async function (client, namespace, dimensions = null) { if (await this.namespaceExists(client, namespace)) { return await client.getCollection(namespace); } + if (!dimensions) + throw new Error( + `Qdrant:getOrCreateCollection Unable to infer vector dimension from input. Open an issue on Github for support.` + ); await client.createCollection(namespace, { vectors: { - size: 1536, //TODO: Fixed to OpenAI models - when other embeddings exist make variable. + size: dimensions, distance: "Cosine", }, }); @@ -127,6 +134,7 @@ const QDrant = { ) { const { DocumentVectors } = require("../../../models/vectors"); try { + let vectorDimension = null; const { pageContent, docId, ...metadata } = documentData; if (!pageContent || pageContent.length == 0) return false; @@ -134,15 +142,20 @@ const QDrant = { const cacheResult = await cachedVectorInformation(fullFilePath); if (cacheResult.exists) { const { client } = await this.connect(); - const collection = await this.getOrCreateCollection(client, namespace); + const { chunks } = cacheResult; + const documentVectors = []; + vectorDimension = chunks[0][0].vector.length || null; + + const collection = await this.getOrCreateCollection( + client, + namespace, + vectorDimension + ); if (!collection) throw new Error("Failed to create new QDrant collection!", { namespace, }); - const { chunks } = cacheResult; - const documentVectors = []; - for (const chunk of chunks) { const submission = { ids: [], @@ -204,6 +217,7 @@ const QDrant = { if (!!vectorValues && vectorValues.length > 0) { for (const [i, vector] of vectorValues.entries()) { + if (!vectorDimension) vectorDimension = vector.length; const vectorRecord = { id: uuidv4(), vector: vector, @@ -227,7 +241,11 @@ const QDrant = { } const { client } = await this.connect(); - const collection = await this.getOrCreateCollection(client, namespace); + const collection = await this.getOrCreateCollection( + client, + namespace, + vectorDimension + ); if (!collection) throw new Error("Failed to create new QDrant collection!", { namespace, diff --git a/server/utils/vectorDbProviders/zilliz/index.js b/server/utils/vectorDbProviders/zilliz/index.js new file mode 100644 index 000000000..b8493e1c2 --- /dev/null +++ b/server/utils/vectorDbProviders/zilliz/index.js @@ -0,0 +1,365 @@ +const { + DataType, + MetricType, + IndexType, + MilvusClient, +} = require("@zilliz/milvus2-sdk-node"); +const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); +const { v4: uuidv4 } = require("uuid"); +const { storeVectorResult, cachedVectorInformation } = require("../../files"); +const { + toChunks, + getLLMProvider, + getEmbeddingEngineSelection, +} = require("../../helpers"); + +// Zilliz is basically a copy of Milvus DB class with a different constructor +// to connect to the cloud +const Zilliz = { + name: "Zilliz", + connect: async function () { + if (process.env.VECTOR_DB !== "zilliz") + throw new Error("Zilliz::Invalid ENV settings"); + + const client = new MilvusClient({ + address: process.env.ZILLIZ_ENDPOINT, + token: process.env.ZILLIZ_API_TOKEN, + }); + + const { isHealthy } = await client.checkHealth(); + if (!isHealthy) + throw new Error( + "Zilliz::Invalid Heartbeat received - is the instance online?" + ); + + return { client }; + }, + heartbeat: async function () { + await this.connect(); + return { heartbeat: Number(new Date()) }; + }, + totalVectors: async function () { + const { client } = await this.connect(); + const { collection_names } = await client.listCollections(); + const total = collection_names.reduce(async (acc, collection_name) => { + const statistics = await client.getCollectionStatistics({ + collection_name, + }); + return Number(acc) + Number(statistics?.data?.row_count ?? 0); + }, 0); + return total; + }, + namespaceCount: async function (_namespace = null) { + const { client } = await this.connect(); + const statistics = await client.getCollectionStatistics({ + collection_name: _namespace, + }); + return Number(statistics?.data?.row_count ?? 0); + }, + namespace: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const collection = await client + .getCollectionStatistics({ collection_name: namespace }) + .catch(() => null); + return collection; + }, + hasNamespace: async function (namespace = null) { + if (!namespace) return false; + const { client } = await this.connect(); + return await this.namespaceExists(client, namespace); + }, + namespaceExists: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const { value } = await client + .hasCollection({ collection_name: namespace }) + .catch((e) => { + console.error("Zilliz::namespaceExists", e.message); + return { value: false }; + }); + return value; + }, + deleteVectorsInNamespace: async function (client, namespace = null) { + await client.dropCollection({ collection_name: namespace }); + return true; + }, + // Zilliz requires a dimension aspect for collection creation + // we pass this in from the first chunk to infer the dimensions like other + // providers do. + getOrCreateCollection: async function (client, namespace, dimensions = null) { + const isExists = await this.namespaceExists(client, namespace); + if (!isExists) { + if (!dimensions) + throw new Error( + `Zilliz:getOrCreateCollection Unable to infer vector dimension from input. Open an issue on Github for support.` + ); + + await client.createCollection({ + collection_name: namespace, + fields: [ + { + name: "id", + description: "id", + data_type: DataType.VarChar, + max_length: 255, + is_primary_key: true, + }, + { + name: "vector", + description: "vector", + data_type: DataType.FloatVector, + dim: dimensions, + }, + { + name: "metadata", + decription: "metadata", + data_type: DataType.JSON, + }, + ], + }); + await client.createIndex({ + collection_name: namespace, + field_name: "vector", + index_type: IndexType.AUTOINDEX, + metric_type: MetricType.COSINE, + }); + await client.loadCollectionSync({ + collection_name: namespace, + }); + } + }, + addDocumentToNamespace: async function ( + namespace, + documentData = {}, + fullFilePath = null + ) { + const { DocumentVectors } = require("../../../models/vectors"); + try { + let vectorDimension = null; + const { pageContent, docId, ...metadata } = documentData; + if (!pageContent || pageContent.length == 0) return false; + + console.log("Adding new vectorized document into namespace", namespace); + const cacheResult = await cachedVectorInformation(fullFilePath); + if (cacheResult.exists) { + const { client } = await this.connect(); + const { chunks } = cacheResult; + const documentVectors = []; + vectorDimension = chunks[0][0].values.length || null; + + await this.getOrCreateCollection(client, namespace, vectorDimension); + for (const chunk of chunks) { + // Before sending to Pinecone and saving the records to our db + // we need to assign the id of each chunk that is stored in the cached file. + const newChunks = chunk.map((chunk) => { + const id = uuidv4(); + documentVectors.push({ docId, vectorId: id }); + return { id, vector: chunk.values, metadata: chunk.metadata }; + }); + const insertResult = await client.insert({ + collection_name: namespace, + data: newChunks, + }); + + if (insertResult?.status.error_code !== "Success") { + throw new Error( + `Error embedding into Zilliz! Reason:${insertResult?.status.reason}` + ); + } + } + await DocumentVectors.bulkInsert(documentVectors); + await client.flushSync({ collection_names: [namespace] }); + return true; + } + + const textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize: + getEmbeddingEngineSelection()?.embeddingMaxChunkLength || 1_000, + chunkOverlap: 20, + }); + const textChunks = await textSplitter.splitText(pageContent); + + console.log("Chunks created from document:", textChunks.length); + const LLMConnector = getLLMProvider(); + const documentVectors = []; + const vectors = []; + const vectorValues = await LLMConnector.embedChunks(textChunks); + + if (!!vectorValues && vectorValues.length > 0) { + for (const [i, vector] of vectorValues.entries()) { + if (!vectorDimension) vectorDimension = vector.length; + const vectorRecord = { + id: uuidv4(), + values: vector, + // [DO NOT REMOVE] + // LangChain will be unable to find your text if you embed manually and dont include the `text` key. + metadata: { ...metadata, text: textChunks[i] }, + }; + + vectors.push(vectorRecord); + documentVectors.push({ docId, vectorId: vectorRecord.id }); + } + } else { + throw new Error( + "Could not embed document chunks! This document will not be recorded." + ); + } + + if (vectors.length > 0) { + const chunks = []; + const { client } = await this.connect(); + await this.getOrCreateCollection(client, namespace, vectorDimension); + + console.log("Inserting vectorized chunks into Zilliz."); + for (const chunk of toChunks(vectors, 100)) { + chunks.push(chunk); + const insertResult = await client.insert({ + collection_name: namespace, + data: chunk.map((item) => ({ + id: item.id, + vector: item.values, + metadata: chunk.metadata, + })), + }); + + if (insertResult?.status.error_code !== "Success") { + throw new Error( + `Error embedding into Zilliz! Reason:${insertResult?.status.reason}` + ); + } + } + await storeVectorResult(chunks, fullFilePath); + await client.flushSync({ collection_names: [namespace] }); + } + + await DocumentVectors.bulkInsert(documentVectors); + return true; + } catch (e) { + console.error(e); + console.error("addDocumentToNamespace", e.message); + return false; + } + }, + deleteDocumentFromNamespace: async function (namespace, docId) { + const { DocumentVectors } = require("../../../models/vectors"); + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) return; + const knownDocuments = await DocumentVectors.where({ docId }); + if (knownDocuments.length === 0) return; + + const vectorIds = knownDocuments.map((doc) => doc.vectorId); + const queryIn = vectorIds.map((v) => `'${v}'`).join(","); + await client.deleteEntities({ + collection_name: namespace, + expr: `id in [${queryIn}]`, + }); + + const indexes = knownDocuments.map((doc) => doc.id); + await DocumentVectors.deleteIds(indexes); + + // Even after flushing Zilliz can take some time to re-calc the count + // so all we can hope to do is flushSync so that the count can be correct + // on a later call. + await client.flushSync({ collection_names: [namespace] }); + return true; + }, + performSimilaritySearch: async function ({ + namespace = null, + input = "", + LLMConnector = null, + similarityThreshold = 0.25, + }) { + if (!namespace || !input || !LLMConnector) + throw new Error("Invalid request to performSimilaritySearch."); + + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) { + return { + contextTexts: [], + sources: [], + message: "Invalid query - no documents found for workspace!", + }; + } + + const queryVector = await LLMConnector.embedTextInput(input); + const { contextTexts, sourceDocuments } = await this.similarityResponse( + client, + namespace, + queryVector, + similarityThreshold + ); + + const sources = sourceDocuments.map((metadata, i) => { + return { ...metadata, text: contextTexts[i] }; + }); + return { + contextTexts, + sources: this.curateSources(sources), + message: false, + }; + }, + similarityResponse: async function ( + client, + namespace, + queryVector, + similarityThreshold = 0.25 + ) { + const result = { + contextTexts: [], + sourceDocuments: [], + scores: [], + }; + const response = await client.search({ + collection_name: namespace, + vectors: queryVector, + }); + response.results.forEach((match) => { + if (match.score < similarityThreshold) return; + result.contextTexts.push(match.metadata.text); + result.sourceDocuments.push(match); + result.scores.push(match.score); + }); + return result; + }, + "namespace-stats": async function (reqBody = {}) { + const { namespace = null } = reqBody; + if (!namespace) throw new Error("namespace required"); + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error("Namespace by that name does not exist."); + const stats = await this.namespace(client, namespace); + return stats + ? stats + : { message: "No stats were able to be fetched from DB for namespace" }; + }, + "delete-namespace": async function (reqBody = {}) { + const { namespace = null } = reqBody; + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error("Namespace by that name does not exist."); + + const statistics = await this.namespace(client, namespace); + await this.deleteVectorsInNamespace(client, namespace); + const vectorCount = Number(statistics?.data?.row_count ?? 0); + return { + message: `Namespace ${namespace} was deleted along with ${vectorCount} vectors.`, + }; + }, + curateSources: function (sources = []) { + const documents = []; + for (const source of sources) { + const { metadata = {} } = source; + if (Object.keys(metadata).length > 0) { + documents.push({ + ...metadata, + ...(source.hasOwnProperty("pageContent") + ? { text: source.pageContent } + : {}), + }); + } + } + + return documents; + }, +}; + +module.exports.Zilliz = Zilliz; diff --git a/server/yarn.lock b/server/yarn.lock index 6215bf01f..cc129dfe9 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -160,6 +160,20 @@ "@azure/logger" "^1.0.3" tslib "^2.4.0" +"@colors/colors@1.6.0", "@colors/colors@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" + integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== + +"@dabh/diagnostics@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" + integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -214,6 +228,35 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== +"@grpc/grpc-js@1.8.17": + version "1.8.17" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.8.17.tgz#a3a2f826fc033eae7d2f5ee41e0ab39cee948838" + integrity sha512-DGuSbtMFbaRsyffMf+VEkVu8HkSXEUfO3UyGJNtqxW9ABdtTIA+2UXAJpwbJS+xfQxuwqLUeELmL6FuZkOqPxw== + dependencies: + "@grpc/proto-loader" "^0.7.0" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@0.7.7": + version "0.7.7" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.7.tgz#d33677a77eea8407f7c66e2abd97589b60eb4b21" + integrity sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ== + dependencies: + "@types/long" "^4.0.1" + lodash.camelcase "^4.3.0" + long "^4.0.0" + protobufjs "^7.0.0" + yargs "^17.7.2" + +"@grpc/proto-loader@^0.7.0": + version "0.7.10" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.10.tgz#6bf26742b1b54d0a473067743da5d3189d06d720" + integrity sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.4" + yargs "^17.7.2" + "@hapi/hoek@^9.0.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -226,6 +269,11 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@huggingface/jinja@^0.1.0": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@huggingface/jinja/-/jinja-0.1.2.tgz#073fa0a68ef481a1806b0186bbafd8013e586fbe" + integrity sha512-x5mpbfJt1nKmVep5WNP5VjNsjWApWNj8pPYI+uYMkBWH9bWUJmQmHt2lbf0VCoQd54Oq3XuFEh/UyoVh7rPxmg== + "@humanwhocodes/config-array@^0.11.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -755,6 +803,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.5.tgz#4a13a6445862159303fc38586598a9396fc408b3" integrity sha512-CRT4tMK/DHYhw1fcCEBwME9CSaZNclxfzVMe7GsO6ULSwsttbj70wSiX6rZdIjGblu93sTJxLdhNIT85KKI7Qw== +"@types/node@>=12.12.47": + version "20.10.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.8.tgz#f1e223cbde9e25696661d167a5b93a9b2a5d57c7" + integrity sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA== + dependencies: + undici-types "~5.26.4" + "@types/node@>=13.7.0": version "20.10.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.3.tgz#4900adcc7fc189d5af5bb41da8f543cea6962030" @@ -779,6 +834,11 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/triple-beam@^1.3.2": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" + integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== + "@types/uuid@^9.0.1": version "9.0.7" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.7.tgz#b14cebc75455eeeb160d5fe23c2fcc0c64f724d8" @@ -796,16 +856,29 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@xenova/transformers@^2.10.0": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@xenova/transformers/-/transformers-2.10.0.tgz#ae97d724a3addf78de7314336a9f7b28ed96a140" - integrity sha512-Al9WKiOsimAC3mU9Ef434GkHF0izmeAM7mMMx5npdWsWLAYL8fmJXCrULj6uCfjomMQ7jyN9rDtKpp570hffiw== +"@xenova/transformers@^2.14.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@xenova/transformers/-/transformers-2.14.0.tgz#6fe128957e64377ca4fca910e77f6092f3f3512a" + integrity sha512-rQ3O7SW5EM64b6XFZGx3XQ2cfiroefxUwU9ShfSpEZyhd082GvwNJJKndxgaukse1hZP1JUDoT0DfjDiq4IZiw== dependencies: + "@huggingface/jinja" "^0.1.0" onnxruntime-web "1.14.0" sharp "^0.32.0" optionalDependencies: onnxruntime-node "1.14.0" +"@zilliz/milvus2-sdk-node@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@zilliz/milvus2-sdk-node/-/milvus2-sdk-node-2.3.5.tgz#6540bc03ebb99ab35f63e4eca7a1fd3ede2cf38c" + integrity sha512-bWbQnhvu+7jZXoqI+qySycwph3vloy0LDV54TBY4wRmu6HhMlqIqyIiI8sQNeSJFs8M1jHg1PlmhE/dvckA1bA== + dependencies: + "@grpc/grpc-js" "1.8.17" + "@grpc/proto-loader" "0.7.7" + dayjs "^1.11.7" + lru-cache "^9.1.2" + protobufjs "7.2.4" + winston "^3.9.0" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1487,7 +1560,7 @@ cmake-js@^7.2.1: which "^2.0.2" yargs "^17.6.0" -color-convert@^1.9.0: +color-convert@^1.9.0, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -1511,7 +1584,7 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.9.0: +color-string@^1.6.0, color-string@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== @@ -1524,6 +1597,14 @@ color-support@^1.1.2, color-support@^1.1.3: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== +color@^3.1.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + color@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" @@ -1537,6 +1618,14 @@ colors@^1.4.0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== +colorspace@1.1.x: + version "1.1.4" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== + dependencies: + color "^3.1.3" + text-hex "1.0.x" + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -1680,6 +1769,11 @@ crypt@0.0.2: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== +dayjs@^1.11.7: + version "1.11.10" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" + integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -1835,6 +1929,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + encode32@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/encode32/-/encode32-1.1.0.tgz#0c54b45fb314ad5502e3c230cb95acdc5e5cd1dd" @@ -2254,6 +2353,11 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fecha@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" + integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2339,6 +2443,11 @@ flow-remove-types@^2.217.1: pirates "^3.0.2" vlq "^0.2.1" +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + follow-redirects@^1.14.8, follow-redirects@^1.14.9: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" @@ -3344,6 +3453,11 @@ keyv@^4.5.3: dependencies: json-buffer "3.0.1" +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + ky@^0.33.1: version "0.33.3" resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543" @@ -3500,11 +3614,28 @@ log-symbols@^5.1.0: chalk "^5.0.0" is-unicode-supported "^1.1.0" +logform@^2.3.2, logform@^2.4.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.6.0.tgz#8c82a983f05d6eaeb2d75e3decae7a768b2bf9b5" + integrity sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ== + dependencies: + "@colors/colors" "1.6.0" + "@types/triple-beam" "^1.3.2" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -3524,6 +3655,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.2.tgz#255fdbc14b75589d6d0e73644ca167a8db506835" + integrity sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ== + make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -4042,6 +4178,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + onetime@^5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" @@ -4334,6 +4477,24 @@ prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +protobufjs@7.2.4: + version "7.2.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.4.tgz#3fc1ec0cdc89dd91aef9ba6037ba07408485c3ae" + integrity sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + protobufjs@^6.8.8: version "6.11.4" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa" @@ -4353,6 +4514,24 @@ protobufjs@^6.8.8: "@types/node" ">=13.7.0" long "^4.0.0" +protobufjs@^7.0.0, protobufjs@^7.2.4: + version "7.2.5" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.5.tgz#45d5c57387a6d29a17aab6846dcc283f9b8e7f2d" + integrity sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -4605,6 +4784,11 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +safe-stable-stringify@^2.3.1: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -4835,6 +5019,11 @@ ssri@^8.0.0, ssri@^8.0.1: dependencies: minipass "^3.1.1" +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -5078,6 +5267,11 @@ tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: mkdirp "^1.0.3" yallist "^4.0.0" +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -5107,6 +5301,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +triple-beam@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" + integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== + tslib@^2.2.0, tslib@^2.4.0: version "2.6.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" @@ -5448,6 +5647,32 @@ wide-align@^1.1.2, wide-align@^1.1.5: dependencies: string-width "^1.0.2 || 2 || 3 || 4" +winston-transport@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.6.0.tgz#f1c1a665ad1b366df72199e27892721832a19e1b" + integrity sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg== + dependencies: + logform "^2.3.2" + readable-stream "^3.6.0" + triple-beam "^1.3.0" + +winston@^3.9.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.11.0.tgz#2d50b0a695a2758bb1c95279f0a88e858163ed91" + integrity sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g== + dependencies: + "@colors/colors" "^1.6.0" + "@dabh/diagnostics" "^2.0.2" + async "^3.2.3" + is-stream "^2.0.0" + logform "^2.4.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.5.0" + wordwrapjs@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f"