From 514f625b8f371bd0262222f03c96f21243f769c0 Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Sun, 16 Jul 2023 12:19:47 -0400 Subject: [PATCH] feat: ChatGPT Plugins/OpenAPI specs for Plugins Endpoint (#620) * wip: proof of concept for openapi chain * chore(api): update langchain dependency to version 0.0.105 * feat(Plugins): use ChatGPT Plugins/OpenAPI specs (first pass) * chore(manifest.json): update pluginKey for "Browser" tool to "web-browser" chore(handleTools.js): update customConstructor key for "web-browser" tool * fix(handleSubmit.js): set unfinished property to false for all endpoints * fix(handlers.js): remove unnecessary capitalizeWords function and use action.tool directly refactor(endpoints.js): rename availableTools to tools and transform it into a map * feat(endpoints): add plugins selector to endpoints file refactor(CodeBlock.tsx): refactor to typescript refactor(Plugin.tsx): use recoil Map for plugin name and refactor to typescript chore(Message.jsx): linting chore(PluginsOptions/index.jsx): remove comment/linting chore(svg): export Clipboard and CheckMark components from SVG index and refactor to typescript * fix(OpenAPIPlugin.js): rename readYamlFile function to readSpecFile fix(OpenAPIPlugin.js): handle JSON files in readSpecFile function fix(OpenAPIPlugin.js): handle JSON URLs in getSpec function fix(OpenAPIPlugin.js): handle JSON variables in createOpenAPIPlugin function fix(OpenAPIPlugin.js): add description for variables in createOpenAPIPlugin function fix(OpenAPIPlugin.js): add optional flag for is_user_authenticated and has_user_authentication in ManifestDefinition fix(loadSpecs.js): add optional flag for is_user_authenticated and has_user_authentication in ManifestDefinition fix(Plugin.tsx): remove unnecessary callback parameter in getPluginName function fix(getDefaultConversation.js): fix browser console error: handle null value for lastConversationSetup in getDefaultConversation function * feat(api): add new tools Add Ai PDF tool for super-fast, interactive chats with PDFs of any size, complete with page references for fact checking. Add VoxScript tool for searching through YouTube transcripts, financial data sources, Google Search results, and more. Add WebPilot tool for browsing and QA of webpages, PDFs, and data. Generate articles from one or more URLs. feat(api): update OpenAPIPlugin.js - Add support for bearer token authorization in the OpenAPIPlugin. - Add support for custom headers in the OpenAPIPlugin. fix(api): fix loadTools.js - Pass the user parameter to the loadSpecs function. * feat(PluginsClient.js): import findMessageContent function from utils feat(PluginsClient.js): add message parameter to options object in initializeCustomAgent function feat(PluginsClient.js): add content to errorMessage if message content is found feat(PluginsClient.js): break out of loop if message content is found feat(PluginsClient.js): add delay option with value of 8 to generateTextStream function feat(PluginsClient.js): add support for process.env.PORT environment variable in app.listen function feat(askyourpdf.json): add askyourpdf plugin configuration feat(metar.json): add metar plugin configuration feat(askyourpdf.yaml): add askyourpdf plugin OpenAPI specification feat(OpenAPIPlugin.js): add message parameter to createOpenAPIPlugin function feat(OpenAPIPlugin.js): add description_for_model to chain run message feat(addOpenAPISpecs.js): remove verbose option from loadSpecs function call fix(loadSpecs.js): add 'message' parameter to the loadSpecs function feat(findMessageContent.js): add utility function to find message content in JSON objects * fix(PluginStoreDialog.tsx): update z-index value for the dialog container The z-index value for the dialog container was updated to "102" to ensure it appears above other elements on the page. * chore(web_pilot.json): add "params" field with "user_has_request" parameter set to true * chore(eslintrc.js): update eslint rules fix(Login.tsx): add missing semicolon after import statement * fix(package-lock.json): update langchain dependency to version ^0.0.105 * fix(OpenAPIPlugin.js): change header key from 'id' to 'librechat_user_id' for consistency and clarity feat(plugins): add documentation for using official ChatGPT Plugins with OpenAPI specs This commit adds a new file `chatgpt_plugins_openapi.md` to the `docs/features/plugins` directory. The file provides detailed information on how to use official ChatGPT Plugins with OpenAPI specifications. It explains the components of a plugin, including the Plugin Manifest file and the OpenAPI spec. It also covers the process of adding a plugin, editing manifest files, and customizing OpenAPI spec files. Additionally, the commit includes disclaimers about the limitations and compatibility of plugins with LibreChat. The documentation also clarifies that the use of ChatGPT Plugins with LibreChat does not violate OpenAI's Terms of Service. The purpose of this commit is to provide comprehensive documentation for developers who want to integrate ChatGPT Plugins into their projects using OpenAPI specs. It aims to guide them through the process of adding and configuring plugins, as well as addressing potential issues and chore(introduction.md): update link to ChatGPT Plugins documentation docs(introduction.md): clarify the purpose of the plugins endpoint and its capabilities * fix(OpenAPIPlugin.js): update SUFFIX variable to provide a clearer description docs(chatgpt_plugins_openapi.md): update information about adding plugins via url on the frontend * feat(PluginsClient.js): sendIntermediateMessage on successful Agent load fix(PluginsClient.js, server/index.js, gptPlugins.js): linting fixes docs(chatgpt_plugins_openapi.md): update links and add additional information * Update chatgpt_plugins_openapi.md * chore: rebuild package-lock file * chore: format/lint all files with new rules * chore: format all files * chore(README.md): update AI model selection list The AI model selection list in the README.md file has been updated to reflect the current options available. The "Anthropic" model has been added as an alternative name for the "Claude" model. * fix(Plugin.tsx): type issue * feat(tools): add new tool WebPilot feat(tools): remove tool Weather Report feat(tools): add new tool Prompt Perfect feat(tools): add new tool Scholarly Graph Link * feat(OpenAPIPlugin.js): add getSpec and readSpecFile functions feat(OpenAPIPlugin.spec.js): add tests for readSpecFile, getSpec, and createOpenAPIPlugin functions * chore(agent-demo-1.js): remove unused code and dependencies chore(agent-demo-2.js): remove unused code and dependencies chore(demo.js): remove unused code and dependencies * feat(addOpenAPISpecs): add function to transform OpenAPI specs into desired format feat(addOpenAPISpecs.spec): add tests for transformSpec function fix(loadSpecs): remove debugging code * feat(loadSpecs.spec.js): add unit tests for ManifestDefinition, validateJson, and loadSpecs functions * fix: package file resolution bug * chore: move scholarly_graph_link manifest to 'has-issues' * refactor(client/hooks): convert to TS and export from index * Update introduction.md * Update chatgpt_plugins_openapi.md --- .eslintrc.js | 4 +- README.md | 2 +- api/app/bingai.js | 4 +- api/app/clients/PluginsClient.js | 19 +- api/app/clients/tools/.well-known/Ai_PDF.json | 18 ++ .../clients/tools/.well-known/VoxScript.json | 22 ++ .../clients/tools/.well-known/askyourpdf.json | 18 ++ .../has-issues/scholarly_graph_link.json | 18 ++ .../.well-known/has-issues/web_pilot.json | 24 ++ .../tools/.well-known/openapi/askyourpdf.yaml | 157 ++++++++++ .../tools/.well-known/openapi/scholarai.yaml | 185 ++++++++++++ .../clients/tools/.well-known/rephrase.json | 18 ++ .../clients/tools/.well-known/scholarai.json | 22 ++ .../clients/tools/dynamic/OpenAPIPlugin.js | 139 +++++++++ .../tools/dynamic/OpenAPIPlugin.spec.js | 65 ++++ api/app/clients/tools/manifest.json | 2 +- api/app/clients/tools/util/addOpenAPISpecs.js | 31 ++ .../tools/util/addOpenAPISpecs.spec.js | 76 +++++ api/app/clients/tools/util/handleTools.js | 19 +- api/app/clients/tools/util/loadSpecs.js | 104 +++++++ api/app/clients/tools/util/loadSpecs.spec.js | 101 +++++++ api/lib/db/migrateDb.js | 12 +- api/lib/parse/citeText.js | 8 +- api/lib/parse/getCitations.js | 8 +- api/lib/utils/misc.js | 4 +- api/models/Message.js | 6 +- api/models/User.js | 11 +- api/models/index.js | 8 +- api/models/plugins/mongoMeili.js | 12 +- api/models/schema/defaults.js | 2 +- api/models/schema/pluginAuthSchema.js | 2 +- api/package.json | 2 +- api/server/controllers/AuthController.js | 6 +- api/server/controllers/ErrorController.js | 8 +- api/server/controllers/PluginController.js | 34 +-- .../controllers/auth/LoginController.js | 23 +- .../controllers/auth/LogoutController.js | 3 +- api/server/index.js | 31 +- api/server/routes/__tests__/config.spec.js | 16 +- api/server/routes/ask/anthropic.js | 10 +- api/server/routes/ask/askChatGPTBrowser.js | 14 +- api/server/routes/ask/bingAI.js | 20 +- api/server/routes/ask/google.js | 8 +- api/server/routes/ask/gptPlugins.js | 32 +- api/server/routes/ask/handlers.js | 25 +- api/server/routes/ask/openAI.js | 30 +- api/server/routes/config.js | 13 +- api/server/routes/convos.js | 7 +- api/server/routes/endpoints.js | 78 ++++- api/server/routes/presets.js | 4 +- api/server/services/auth.service.js | 4 +- api/strategies/openidStrategy.js | 26 +- api/utils/LoggingSystem.js | 44 ++- api/utils/abortMessage.js | 2 +- api/utils/azureUtils.js | 8 +- api/utils/debug.js | 20 +- api/utils/findMessageContent.js | 33 ++ api/utils/index.js | 4 +- client/src/components/Auth/Login.tsx | 20 +- client/src/components/Auth/Registration.tsx | 18 +- .../components/Auth/RequestPasswordReset.tsx | 4 +- client/src/components/Auth/ResetPassword.tsx | 11 +- .../Auth/__tests__/LoginForm.spec.tsx | 5 +- .../components/Conversations/DeleteButton.jsx | 4 +- .../Endpoints/Anthropic/OptionHover.jsx | 5 +- .../components/Endpoints/EditPresetDialog.jsx | 6 +- .../Endpoints/EndpointOptionsPopover.jsx | 7 +- .../components/Endpoints/Google/Settings.jsx | 11 +- .../Endpoints/OpenAI/OptionHover.jsx | 6 +- .../components/Endpoints/OpenAI/Settings.jsx | 6 +- .../Endpoints/Plugins/AgentSettings.jsx | 33 +- .../components/Endpoints/Plugins/Settings.jsx | 8 +- .../src/components/Endpoints/Plugins/index.ts | 2 +- client/src/components/Endpoints/Settings.jsx | 2 +- .../components/Input/BingAIOptions/index.jsx | 8 +- .../components/Input/ChatGPTOptions/index.jsx | 8 +- .../NewConversationMenu/EndpointItem.jsx | 2 +- .../Input/NewConversationMenu/FileUpload.tsx | 4 +- .../Input/NewConversationMenu/PresetItem.jsx | 32 +- .../Input/NewConversationMenu/index.jsx | 4 +- .../components/Input/PluginsOptions/index.jsx | 21 +- .../Input/SetTokenDialog/GoogleConfig.tsx | 14 +- .../Input/SetTokenDialog/HelpText.tsx | 27 +- .../Input/SetTokenDialog/InputWithLabel.tsx | 2 +- .../Input/SetTokenDialog/OpenAIConfig.tsx | 20 +- .../Input/SetTokenDialog/OtherConfig.tsx | 2 +- .../Input/SetTokenDialog/SetTokenDialog.tsx | 16 +- .../components/Input/SetTokenDialog/index.ts | 2 +- client/src/components/Input/SubmitButton.jsx | 2 +- client/src/components/Input/index.jsx | 4 +- .../src/components/MessageHandler/index.jsx | 17 +- .../Content/{CodeBlock.jsx => CodeBlock.tsx} | 34 ++- .../components/Messages/Content/Content.jsx | 2 +- .../src/components/Messages/HoverButtons.jsx | 12 +- client/src/components/Messages/Message.jsx | 13 +- .../src/components/Messages/MessageHeader.jsx | 38 ++- .../src/components/Messages/MultiMessage.jsx | 3 +- client/src/components/Messages/Plugin.jsx | 92 ------ client/src/components/Messages/Plugin.tsx | 146 +++++++++ client/src/components/Messages/index.jsx | 2 +- .../Nav/ExportConversation/ExportModel.jsx | 80 +++-- .../Nav/ExportConversation/index.jsx | 6 +- client/src/components/Nav/NavLinks.jsx | 17 +- client/src/components/Nav/NewChat.jsx | 2 +- client/src/components/Nav/SearchBar.jsx | 14 +- .../Nav/SettingsTabs/ThemeSelector.spec.tsx | 4 +- .../src/components/Nav/SettingsTabs/index.ts | 2 +- client/src/components/Nav/index.jsx | 5 +- .../Plugins/Store/PluginStoreDialog.tsx | 2 +- .../Store/__tests__/PluginAuthForm.spec.tsx | 2 +- .../Store/__tests__/PluginPagination.spec.tsx | 2 +- .../Store/__tests__/PluginStoreItem.spec.tsx | 11 +- client/src/components/Plugins/Store/index.ts | 2 +- client/src/components/svg/BingIcon.jsx | 282 ++++++++++++++++++ client/src/components/svg/BingJbIcon.jsx | 267 +++++++++++++++++ .../svg/{Clipboard.jsx => Clipboard.tsx} | 0 client/src/components/svg/CogIcon.tsx | 2 +- client/src/components/svg/DiscordIcon.jsx | 12 +- client/src/components/svg/GoogleIcon.jsx | 9 +- client/src/components/svg/OpenIDIcon.jsx | 3 +- client/src/components/svg/index.ts | 2 + client/src/components/ui/AlertDialog.tsx | 2 +- client/src/components/ui/Dialog.tsx | 9 +- .../src/components/ui/DialogTemplate.spec.tsx | 4 +- client/src/components/ui/DialogTemplate.tsx | 4 +- client/src/components/ui/ModelSelect.jsx | 8 +- client/src/components/ui/Switch.tsx | 12 +- client/src/components/ui/Templates.jsx | 8 +- client/src/components/ui/Textarea.tsx | 4 +- client/src/hooks/index.ts | 2 + .../hooks/{useDebounce.js => useDebounce.ts} | 2 +- client/src/hooks/useDocumentTitle.js | 1 - client/src/hooks/useOnClickOutside.js | 2 +- client/src/localization/Translation.tsx | 14 +- client/src/localization/languages/Eng.tsx | 17 +- client/src/localization/languages/It.tsx | 17 +- client/src/localization/languages/Zh.tsx | 17 +- client/src/routes/Chat.jsx | 18 +- client/src/routes/Search.jsx | 16 +- client/src/routes/index.jsx | 2 +- client/src/store/conversation.js | 35 ++- client/src/store/endpoints.js | 23 +- client/src/utils/buildTree.js | 11 +- client/src/utils/getDefaultConversation.js | 5 +- client/src/utils/getError.ts | 5 +- client/src/utils/getIcon.jsx | 6 +- client/src/utils/handleSubmit.js | 7 +- client/src/utils/index.jsx | 1 - client/vite.config.ts | 3 +- config/create-user.js | 28 +- config/helpers.js | 2 +- config/install.js | 30 +- config/loader.js | 59 ++-- config/upgrade.js | 62 ++-- .../plugins/chatgpt_plugins_openapi.md | 175 +++++++++++ docs/features/plugins/introduction.md | 7 +- e2e/setup/authenticate.ts | 2 +- e2e/specs/landing.spec.js | 1 - e2e/specs/messages.spec.js | 5 +- e2e/specs/nav.spec.js | 6 +- e2e/specs/popup.spec.js | 2 +- e2e/specs/settings.spec.js | 4 +- package-lock.json | 43 ++- packages/data-provider/src/index.ts | 2 +- packages/data-provider/src/types.ts | 6 +- 165 files changed, 3003 insertions(+), 713 deletions(-) create mode 100644 api/app/clients/tools/.well-known/Ai_PDF.json create mode 100644 api/app/clients/tools/.well-known/VoxScript.json create mode 100644 api/app/clients/tools/.well-known/askyourpdf.json create mode 100644 api/app/clients/tools/.well-known/has-issues/scholarly_graph_link.json create mode 100644 api/app/clients/tools/.well-known/has-issues/web_pilot.json create mode 100644 api/app/clients/tools/.well-known/openapi/askyourpdf.yaml create mode 100644 api/app/clients/tools/.well-known/openapi/scholarai.yaml create mode 100644 api/app/clients/tools/.well-known/rephrase.json create mode 100644 api/app/clients/tools/.well-known/scholarai.json create mode 100644 api/app/clients/tools/dynamic/OpenAPIPlugin.js create mode 100644 api/app/clients/tools/dynamic/OpenAPIPlugin.spec.js create mode 100644 api/app/clients/tools/util/addOpenAPISpecs.js create mode 100644 api/app/clients/tools/util/addOpenAPISpecs.spec.js create mode 100644 api/app/clients/tools/util/loadSpecs.js create mode 100644 api/app/clients/tools/util/loadSpecs.spec.js create mode 100644 api/utils/findMessageContent.js rename client/src/components/Messages/Content/{CodeBlock.jsx => CodeBlock.tsx} (66%) delete mode 100644 client/src/components/Messages/Plugin.jsx create mode 100644 client/src/components/Messages/Plugin.tsx create mode 100644 client/src/components/svg/BingIcon.jsx create mode 100644 client/src/components/svg/BingJbIcon.jsx rename client/src/components/svg/{Clipboard.jsx => Clipboard.tsx} (100%) create mode 100644 client/src/hooks/index.ts rename client/src/hooks/{useDebounce.js => useDebounce.ts} (86%) create mode 100644 docs/features/plugins/chatgpt_plugins_openapi.md diff --git a/.eslintrc.js b/.eslintrc.js index d1c54c150ff..f0c7505ee50 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,8 +44,10 @@ module.exports = { }, ], 'linebreak-style': 0, - 'object-curly-spacing': ['error', 'always'], + 'curly': ['error', 'all'], + 'semi': ['error', 'always'], 'no-trailing-spaces': 'error', + 'object-curly-spacing': ['error', 'always'], 'no-multiple-empty-lines': ['error', { max: 1 }], 'comma-dangle': ['error', 'always-multiline'], // "arrow-parens": [2, "as-needed", { requireForBlockBody: true }], diff --git a/README.md b/README.md index cb3431ea5db..c0f746d73b1 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ https://github.com/danny-avila/LibreChat/assets/110412045/c1eb0c0f-41f6-4335-b98 # Features - Response streaming identical to ChatGPT through server-sent events - UI from original ChatGPT, including Dark mode -- AI model selection (through 6 endpoints: OpenAI API, BingAI, ChatGPT Browser, PaLM2, Claude, Plugins) +- AI model selection: OpenAI API, BingAI, ChatGPT Browser, PaLM2, Anthropic (Claude), Plugins - Create, Save, & Share custom presets - [More info on prompt presets here](https://github.com/danny-avila/LibreChat/releases/tag/v0.3.0) - Edit and Resubmit messages with conversation branching - Search all messages/conversations - [More info here](https://github.com/danny-avila/LibreChat/releases/tag/v0.1.0) diff --git a/api/app/bingai.js b/api/app/bingai.js index 1db564ceb3f..97674574c9c 100644 --- a/api/app/bingai.js +++ b/api/app/bingai.js @@ -39,7 +39,7 @@ const askBing = async ({ jailbreakConversationId = false; } - if (jailbreak) + if (jailbreak) { options = { jailbreakConversationId: jailbreakConversationId || jailbreak, context, @@ -48,7 +48,7 @@ const askBing = async ({ toneStyle, onProgress, }; - else { + } else { options = { conversationId, context, diff --git a/api/app/clients/PluginsClient.js b/api/app/clients/PluginsClient.js index 43054aa6e01..2e84304c3a2 100644 --- a/api/app/clients/PluginsClient.js +++ b/api/app/clients/PluginsClient.js @@ -2,6 +2,7 @@ const OpenAIClient = require('./OpenAIClient'); const { ChatOpenAI } = require('langchain/chat_models/openai'); const { CallbackManager } = require('langchain/callbacks'); const { initializeCustomAgent, initializeFunctionsAgent } = require('./agents/'); +const { findMessageContent } = require('../../utils'); const { loadTools } = require('./tools/util'); const { SelfReflectionTool } = require('./tools/'); const { HumanChatMessage, AIChatMessage } = require('langchain/schema'); @@ -193,6 +194,8 @@ Only respond with your conversational reply to the following User Message: functions: this.functionsAgent, options: { openAIApiKey: this.openAIApiKey, + debug: this.options?.debug, + message, }, }); // load tools @@ -266,6 +269,15 @@ Only respond with your conversational reply to the following User Message: if (this.options.debug) { console.debug('Loaded agent.'); } + + onAgentAction( + { + tool: 'self-reflection', + toolInput: `Processing the User's message:\n"${message}"`, + log: '', + }, + true, + ); } async executorCall(message, signal) { @@ -290,6 +302,11 @@ Only respond with your conversational reply to the following User Message: } catch (err) { console.error(err); errorMessage = err.message; + const content = findMessageContent(message); + if (content) { + errorMessage = content; + break; + } if (attempts === maxAttempts) { this.result.output = `Encountered an error while attempting to respond. Error: ${err.message}`; this.result.intermediateSteps = this.actions; @@ -408,7 +425,7 @@ Only respond with your conversational reply to the following User Message: if (this.agentOptions.skipCompletion && this.result.output) { responseMessage.text = this.result.output; this.addImages(this.result.intermediateSteps, responseMessage); - await this.generateTextStream(this.result.output, opts.onProgress); + await this.generateTextStream(this.result.output, opts.onProgress, { delay: 8 }); return await this.handleResponseMessage(responseMessage, saveOptions, user); } diff --git a/api/app/clients/tools/.well-known/Ai_PDF.json b/api/app/clients/tools/.well-known/Ai_PDF.json new file mode 100644 index 00000000000..e3caf6e2c75 --- /dev/null +++ b/api/app/clients/tools/.well-known/Ai_PDF.json @@ -0,0 +1,18 @@ +{ + "schema_version": "v1", + "name_for_human": "Ai PDF", + "name_for_model": "Ai_PDF", + "description_for_human": "Super-fast, interactive chats with PDFs of any size, complete with page references for fact checking.", + "description_for_model": "Provide a URL to a PDF and search the document. Break the user question in multiple semantic search queries and calls as needed. Think step by step.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "https://plugin-3c56b9d4c8a6465998395f28b6a445b2-jexkai4vea-uc.a.run.app/openapi.yaml", + "is_user_authenticated": false + }, + "logo_url": "https://plugin-3c56b9d4c8a6465998395f28b6a445b2-jexkai4vea-uc.a.run.app/logo.png", + "contact_email": "support@promptapps.ai", + "legal_info_url": "https://plugin-3c56b9d4c8a6465998395f28b6a445b2-jexkai4vea-uc.a.run.app/legal.html" +} diff --git a/api/app/clients/tools/.well-known/VoxScript.json b/api/app/clients/tools/.well-known/VoxScript.json new file mode 100644 index 00000000000..8691f0ccfd8 --- /dev/null +++ b/api/app/clients/tools/.well-known/VoxScript.json @@ -0,0 +1,22 @@ +{ + "schema_version": "v1", + "name_for_human": "VoxScript", + "name_for_model": "VoxScript", + "description_for_human": "Enables searching of YouTube transcripts, financial data sources Google Search results, and more!", + "description_for_model": "Plugin for searching through varius data sources.", + "auth": { + "type": "service_http", + "authorization_type": "bearer", + "verification_tokens": { + "openai": "ffc5226d1af346c08a98dee7deec9f76" + } + }, + "api": { + "type": "openapi", + "url": "https://voxscript.awt.icu/swagger/v1/swagger.yaml", + "is_user_authenticated": false + }, + "logo_url": "https://voxscript.awt.icu/images/VoxScript_logo_32x32.png", + "contact_email": "voxscript@allwiretech.com", + "legal_info_url": "https://voxscript.awt.icu/legal/" +} diff --git a/api/app/clients/tools/.well-known/askyourpdf.json b/api/app/clients/tools/.well-known/askyourpdf.json new file mode 100644 index 00000000000..0eb31e37c7e --- /dev/null +++ b/api/app/clients/tools/.well-known/askyourpdf.json @@ -0,0 +1,18 @@ +{ + "schema_version": "v1", + "name_for_model": "askyourpdf", + "name_for_human": "AskYourPDF", + "description_for_model": "This plugin is designed to expedite the extraction of information from PDF documents. It works by accepting a URL link to a PDF or a document ID (doc_id) from the user. If a URL is provided, the plugin first validates that it is a correct URL. \\nAfter validating the URL, the plugin proceeds to download the PDF and store its content in a vector database. If the user provides a doc_id, the plugin directly retrieves the document from the database. The plugin then scans through the stored PDFs to find answers to user queries or retrieve specific details.\\n\\nHowever, if an error occurs while querying the API, the user is prompted to download their document first, then manually upload it to [![Upload Document](https://raw.githubusercontent.com/AskYourPdf/ask-plugin/main/upload.png)](https://askyourpdf.com/upload). Once the upload is complete, the user should copy the resulting doc_id and paste it back into the chat for further interaction.\nThe plugin is particularly useful when the user's question pertains to content within a PDF document. When providing answers, the plugin also specifies the page number (highlighted in bold) where the relevant information was found. Remember, the URL must be valid for a successful query. Failure to validate the URL may lead to errors or unsuccessful queries.", + "description_for_human": "Unlock the power of your PDFs!, dive into your documents, find answers, and bring information to your fingertips.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "askyourpdf.yaml", + "has_user_authentication": false + }, + "logo_url": "https://plugin.askyourpdf.com/.well-known/logo.png", + "contact_email": "plugin@askyourpdf.com", + "legal_info_url": "https://askyourpdf.com/terms" +} diff --git a/api/app/clients/tools/.well-known/has-issues/scholarly_graph_link.json b/api/app/clients/tools/.well-known/has-issues/scholarly_graph_link.json new file mode 100644 index 00000000000..8b92e6e3811 --- /dev/null +++ b/api/app/clients/tools/.well-known/has-issues/scholarly_graph_link.json @@ -0,0 +1,18 @@ +{ + "schema_version": "v1", + "name_for_human": "Scholarly Graph Link", + "name_for_model": "scholarly_graph_link", + "description_for_human": "You can search papers, authors, datasets and software. It has access to Figshare, Arxiv, and many others.", + "description_for_model": "Run GraphQL queries against an API hosted by DataCite API. The API supports most GraphQL query but does not support mutations statements. Use `{ __schema { types { name kind } } }` to get all the types in the GraphQL schema. Use `{ datasets { nodes { id sizes citations { nodes { id titles { title } } } } } }` to get all the citations of all datasets in the API. Use `{ datasets { nodes { id sizes citations { nodes { id titles { title } } } } } }` to get all the citations of all datasets in the API. Use `{person(id:ORCID) {works(first:50) {nodes {id titles(first: 1){title} publicationYear}}}}` to get the first 50 works of a person based on their ORCID. All Ids are urls, e.g., https://orcid.org/0012-0000-1012-1110. Mutations statements are not allowed.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "https://api.datacite.org/graphql-openapi.yaml", + "is_user_authenticated": false + }, + "logo_url": "https://raw.githubusercontent.com/kjgarza/scholarly_graph_link/master/logo.png", + "contact_email": "kj.garza@gmail.com", + "legal_info_url": "https://github.com/kjgarza/scholarly_graph_link/blob/master/LICENSE" +} diff --git a/api/app/clients/tools/.well-known/has-issues/web_pilot.json b/api/app/clients/tools/.well-known/has-issues/web_pilot.json new file mode 100644 index 00000000000..d68c919eb36 --- /dev/null +++ b/api/app/clients/tools/.well-known/has-issues/web_pilot.json @@ -0,0 +1,24 @@ +{ + "schema_version": "v1", + "name_for_human": "WebPilot", + "name_for_model": "web_pilot", + "description_for_human": "Browse & QA Webpage/PDF/Data. Generate articles, from one or more URLs.", + "description_for_model": "This tool allows users to provide a URL(or URLs) and optionally requests for interacting with, extracting specific information or how to do with the content from the URL. Requests may include rewrite, translate, and others. If there any requests, when accessing the /api/visit-web endpoint, the parameter 'user_has_request' should be set to 'true. And if there's no any requests, 'user_has_request' should be set to 'false'.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "https://webreader.webpilotai.com/openapi.yaml", + "is_user_authenticated": false + }, + "logo_url": "https://webreader.webpilotai.com/logo.png", + "contact_email": "dev@webpilot.ai", + "legal_info_url": "https://webreader.webpilotai.com/legal_info.html", + "headers": { + "id": "WebPilot-Friend-UID" + }, + "params": { + "user_has_request": true + } +} diff --git a/api/app/clients/tools/.well-known/openapi/askyourpdf.yaml b/api/app/clients/tools/.well-known/openapi/askyourpdf.yaml new file mode 100644 index 00000000000..cb3affc8b8f --- /dev/null +++ b/api/app/clients/tools/.well-known/openapi/askyourpdf.yaml @@ -0,0 +1,157 @@ +openapi: 3.0.2 +info: + title: FastAPI + version: 0.1.0 +servers: + - url: https://plugin.askyourpdf.com +paths: + /api/download_pdf: + post: + summary: Download Pdf + description: Download a PDF file from a URL and save it to the vector database. + operationId: download_pdf_api_download_pdf_post + parameters: + - required: true + schema: + title: Url + type: string + name: url + in: query + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/FileResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /query: + post: + summary: Perform Query + description: Perform a query on a document. + operationId: perform_query_query_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InputData' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/ResponseModel' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' +components: + schemas: + DocumentMetadata: + title: DocumentMetadata + required: + - source + - page_number + - author + type: object + properties: + source: + title: Source + type: string + page_number: + title: Page Number + type: integer + author: + title: Author + type: string + FileResponse: + title: FileResponse + required: + - docId + type: object + properties: + docId: + title: Docid + type: string + error: + title: Error + type: string + HTTPValidationError: + title: HTTPValidationError + type: object + properties: + detail: + title: Detail + type: array + items: + $ref: '#/components/schemas/ValidationError' + InputData: + title: InputData + required: + - doc_id + - query + type: object + properties: + doc_id: + title: Doc Id + type: string + query: + title: Query + type: string + ResponseModel: + title: ResponseModel + required: + - results + type: object + properties: + results: + title: Results + type: array + items: + $ref: '#/components/schemas/SearchResult' + SearchResult: + title: SearchResult + required: + - doc_id + - text + - metadata + type: object + properties: + doc_id: + title: Doc Id + type: string + text: + title: Text + type: string + metadata: + $ref: '#/components/schemas/DocumentMetadata' + ValidationError: + title: ValidationError + required: + - loc + - msg + - type + type: object + properties: + loc: + title: Location + type: array + items: + anyOf: + - type: string + - type: integer + msg: + title: Message + type: string + type: + title: Error Type + type: string diff --git a/api/app/clients/tools/.well-known/openapi/scholarai.yaml b/api/app/clients/tools/.well-known/openapi/scholarai.yaml new file mode 100644 index 00000000000..34cca8296f7 --- /dev/null +++ b/api/app/clients/tools/.well-known/openapi/scholarai.yaml @@ -0,0 +1,185 @@ +openapi: 3.0.1 +info: + title: ScholarAI + description: Allows the user to search facts and findings from scientific articles + version: 'v1' +servers: + - url: https://scholar-ai.net +paths: + /api/abstracts: + get: + operationId: searchAbstracts + summary: Get relevant paper abstracts by keywords search + parameters: + - name: keywords + in: query + description: Keywords of inquiry which should appear in article. Must be in English. + required: true + schema: + type: string + - name: sort + in: query + description: The sort order for results. Valid values are cited_by_count or publication_date. Excluding this value does a relevance based search. + required: false + schema: + type: string + enum: + - cited_by_count + - publication_date + - name: query + in: query + description: The user query + required: true + schema: + type: string + - name: peer_reviewed_only + in: query + description: Whether to only return peer reviewed articles. Defaults to true, ChatGPT should cautiously suggest this value can be set to false + required: false + schema: + type: string + - name: start_year + in: query + description: The first year, inclusive, to include in the search range. Excluding this value will include all years. + required: false + schema: + type: string + - name: end_year + in: query + description: The last year, inclusive, to include in the search range. Excluding this value will include all years. + required: false + schema: + type: string + - name: offset + in: query + description: The offset of the first result to return. Defaults to 0. + required: false + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/searchAbstractsResponse' + /api/fulltext: + get: + operationId: getFullText + summary: Get full text of a paper by URL for PDF + parameters: + - name: pdf_url + in: query + description: URL for PDF + required: true + schema: + type: string + - name: chunk + in: query + description: chunk number to retrieve, defaults to 1 + required: false + schema: + type: number + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/getFullTextResponse' + /api/save-citation: + get: + operationId: saveCitation + summary: Save citation to reference manager + parameters: + - name: doi + in: query + description: Digital Object Identifier (DOI) of article + required: true + schema: + type: string + - name: zotero_user_id + in: query + description: Zotero User ID + required: true + schema: + type: string + - name: zotero_api_key + in: query + description: Zotero API Key + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/saveCitationResponse' +components: + schemas: + searchAbstractsResponse: + type: object + properties: + next_offset: + type: number + description: The offset of the next page of results. + total_num_results: + type: number + description: The total number of results. + abstracts: + type: array + items: + type: object + properties: + title: + type: string + abstract: + type: string + description: Summary of the context, methods, results, and conclusions of the paper. + doi: + type: string + description: The DOI of the paper. + landing_page_url: + type: string + description: Link to the paper on its open-access host. + pdf_url: + type: string + description: Link to the paper PDF. + publicationDate: + type: string + description: The date the paper was published in YYYY-MM-DD format. + relevance: + type: number + description: The relevance of the paper to the search query. 1 is the most relevant. + creators: + type: array + items: + type: string + description: The name of the creator. + cited_by_count: + type: number + description: The number of citations of the article. + description: The list of relevant abstracts. + getFullTextResponse: + type: object + properties: + full_text: + type: string + description: The full text of the paper. + pdf_url: + type: string + description: The PDF URL of the paper. + chunk: + type: number + description: The chunk of the paper. + total_chunk_num: + type: number + description: The total chunks of the paper. + saveCitationResponse: + type: object + properties: + message: + type: string + description: Confirmation of successful save or error message. \ No newline at end of file diff --git a/api/app/clients/tools/.well-known/rephrase.json b/api/app/clients/tools/.well-known/rephrase.json new file mode 100644 index 00000000000..53cf0615400 --- /dev/null +++ b/api/app/clients/tools/.well-known/rephrase.json @@ -0,0 +1,18 @@ +{ + "schema_version": "v1", + "name_for_human": "Prompt Perfect", + "name_for_model": "rephrase", + "description_for_human": "Type 'perfect' to craft the perfect prompt, every time.", + "description_for_model": "Plugin that can rephrase user inputs to improve the quality of ChatGPT's responses. The plugin evaluates user inputs and, if necessary, transforms them into clearer, more specific, and contextual prompts. It processes a JSON object containing the user input to be rephrased and uses the GPT-3.5-turbo model for the rephrasing process. The rephrased input is then returned as raw data to be incorporated into ChatGPT's response. The user can initiate the plugin by typing 'perfect'.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "https://promptperfect.xyz/openapi.yaml", + "is_user_authenticated": false + }, + "logo_url": "https://promptperfect.xyz/static/prompt_perfect_logo.png", + "contact_email": "heyo@promptperfect.xyz", + "legal_info_url": "https://promptperfect.xyz/static/terms.html" +} diff --git a/api/app/clients/tools/.well-known/scholarai.json b/api/app/clients/tools/.well-known/scholarai.json new file mode 100644 index 00000000000..1900a926c24 --- /dev/null +++ b/api/app/clients/tools/.well-known/scholarai.json @@ -0,0 +1,22 @@ +{ + "schema_version": "v1", + "name_for_human": "ScholarAI", + "name_for_model": "scholarai", + "description_for_human": "Unleash scientific research: search 40M+ peer-reviewed papers, explore scientific PDFs, and save to reference managers.", + "description_for_model": "Access open access scientific literature from peer-reviewed journals. The abstract endpoint finds relevant papers based on 2 to 6 keywords. After getting abstracts, ALWAYS prompt the user offering to go into more detail. Use the fulltext endpoint to retrieve the entire paper's text and access specific details using the provided pdf_url, if available. ALWAYS hyperlink the pdf_url from the responses if available. Offer to dive into the fulltext or search for additional papers. Always ask if the user wants save any paper to the user’s Zotero reference manager by using the save-citation endpoint and providing the doi and requesting the user’s zotero_user_id and zotero_api_key.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "scholarai.yaml", + "is_user_authenticated": false + }, + "params": { + "sort": "cited_by_count" + }, + "logo_url": "https://scholar-ai.net/logo.png", + "contact_email": "lakshb429@gmail.com", + "legal_info_url": "https://scholar-ai.net/legal.txt", + "HttpAuthorizationType": "basic" +} diff --git a/api/app/clients/tools/dynamic/OpenAPIPlugin.js b/api/app/clients/tools/dynamic/OpenAPIPlugin.js new file mode 100644 index 00000000000..6d00d490d5d --- /dev/null +++ b/api/app/clients/tools/dynamic/OpenAPIPlugin.js @@ -0,0 +1,139 @@ +require('dotenv').config(); +const { z } = require('zod'); +const fs = require('fs'); +const yaml = require('js-yaml'); +const path = require('path'); +const { DynamicStructuredTool } = require('langchain/tools'); +const { createOpenAPIChain } = require('langchain/chains'); +const SUFFIX = 'Prioritize using responses for subsequent requests to better fulfill the query.'; + +const AuthBearer = z + .object({ + type: z.string().includes('service_http'), + authorization_type: z.string().includes('bearer'), + verification_tokens: z.object({ + openai: z.string(), + }), + }) + .catch(() => false); + +const AuthDefinition = z + .object({ + type: z.string(), + authorization_type: z.string(), + verification_tokens: z.object({ + openai: z.string(), + }), + }) + .catch(() => false); + +async function readSpecFile(filePath) { + try { + const fileContents = await fs.promises.readFile(filePath, 'utf8'); + if (path.extname(filePath) === '.json') { + return JSON.parse(fileContents); + } + return yaml.load(fileContents); + } catch (e) { + console.error(e); + return false; + } +} + +async function getSpec(url) { + const RegularUrl = z + .string() + .url() + .catch(() => false); + + if (RegularUrl.parse(url) && path.extname(url) === '.json') { + const response = await fetch(url); + return await response.json(); + } + + const ValidSpecPath = z + .string() + .url() + .catch(async () => { + const spec = path.join(__dirname, '..', '.well-known', 'openapi', url); + if (!fs.existsSync(spec)) { + return false; + } + + return await readSpecFile(spec); + }); + + return ValidSpecPath.parse(url); +} + +async function createOpenAPIPlugin({ data, llm, user, message, verbose = false }) { + let spec; + try { + spec = await getSpec(data.api.url, verbose); + } catch (error) { + verbose && console.debug('getSpec error', error); + return null; + } + + if (!spec) { + verbose && console.debug('No spec found'); + return null; + } + + const headers = {}; + const { auth, description_for_model } = data; + if (auth && AuthDefinition.parse(auth)) { + verbose && console.debug('auth detected', auth); + const { openai } = auth.verification_tokens; + if (AuthBearer.parse(auth)) { + headers.authorization = `Bearer ${openai}`; + verbose && console.debug('added auth bearer', headers); + } + } + + return new DynamicStructuredTool({ + name: data.name_for_model, + description: `${data.description_for_human} ${SUFFIX}`, + schema: z.object({ + query: z + .string() + .describe( + 'For the query, be specific in a conversational manner. It will be interpreted by a human.', + ), + }), + func: async () => { + const chainOptions = { + llm, + verbose, + }; + + if (data.headers && data.headers['librechat_user_id']) { + verbose && console.debug('id detected', headers); + headers[data.headers['librechat_user_id']] = user; + } + + if (Object.keys(headers).length > 0) { + verbose && console.debug('headers detected', headers); + chainOptions.headers = headers; + } + + if (data.params) { + verbose && console.debug('params detected', data.params); + chainOptions.params = data.params; + } + + const chain = await createOpenAPIChain(spec, chainOptions); + const result = await chain.run( + `${message}\n\n||>Instructions: ${description_for_model}\n${SUFFIX}`, + ); + console.log('api chain run result', result); + return result; + }, + }); +} + +module.exports = { + getSpec, + readSpecFile, + createOpenAPIPlugin, +}; diff --git a/api/app/clients/tools/dynamic/OpenAPIPlugin.spec.js b/api/app/clients/tools/dynamic/OpenAPIPlugin.spec.js new file mode 100644 index 00000000000..5fe7f1cb364 --- /dev/null +++ b/api/app/clients/tools/dynamic/OpenAPIPlugin.spec.js @@ -0,0 +1,65 @@ +const fs = require('fs'); +const { createOpenAPIPlugin, getSpec, readSpecFile } = require('./OpenAPIPlugin'); + +jest.mock('node-fetch'); +jest.mock('fs', () => ({ + promises: { + readFile: jest.fn(), + }, + existsSync: jest.fn(), +})); + +describe('readSpecFile', () => { + it('reads JSON file correctly', async () => { + fs.promises.readFile.mockResolvedValue(JSON.stringify({ test: 'value' })); + const result = await readSpecFile('test.json'); + expect(result).toEqual({ test: 'value' }); + }); + + it('reads YAML file correctly', async () => { + fs.promises.readFile.mockResolvedValue('test: value'); + const result = await readSpecFile('test.yaml'); + expect(result).toEqual({ test: 'value' }); + }); + + it('handles error correctly', async () => { + fs.promises.readFile.mockRejectedValue(new Error('test error')); + const result = await readSpecFile('test.json'); + expect(result).toBe(false); + }); +}); + +describe('getSpec', () => { + it('fetches spec from url correctly', async () => { + const parsedJson = await getSpec('https://www.instacart.com/.well-known/ai-plugin.json'); + const isObject = typeof parsedJson === 'object'; + expect(isObject).toEqual(true); + }); + + it('reads spec from file correctly', async () => { + fs.existsSync.mockReturnValue(true); + fs.promises.readFile.mockResolvedValue(JSON.stringify({ test: 'value' })); + const result = await getSpec('test.json'); + expect(result).toEqual({ test: 'value' }); + }); + + it('returns false when file does not exist', async () => { + fs.existsSync.mockReturnValue(false); + const result = await getSpec('test.json'); + expect(result).toBe(false); + }); +}); + +describe('createOpenAPIPlugin', () => { + it('returns null when getSpec throws an error', async () => { + const result = await createOpenAPIPlugin({ data: { api: { url: 'invalid' } } }); + expect(result).toBe(null); + }); + + it('returns null when no spec is found', async () => { + const result = await createOpenAPIPlugin({}); + expect(result).toBe(null); + }); + + // Add more tests here for different scenarios +}); diff --git a/api/app/clients/tools/manifest.json b/api/app/clients/tools/manifest.json index 70fcd76c34a..a2968135cb7 100644 --- a/api/app/clients/tools/manifest.json +++ b/api/app/clients/tools/manifest.json @@ -32,7 +32,7 @@ }, { "name": "Browser", - "pluginKey": "browser", + "pluginKey": "web-browser", "description": "Scrape and summarize webpage data", "icon": "/assets/web-browser.svg", "authConfig": [ diff --git a/api/app/clients/tools/util/addOpenAPISpecs.js b/api/app/clients/tools/util/addOpenAPISpecs.js new file mode 100644 index 00000000000..2d5756f1948 --- /dev/null +++ b/api/app/clients/tools/util/addOpenAPISpecs.js @@ -0,0 +1,31 @@ +const { loadSpecs } = require('./loadSpecs'); + +function transformSpec(input) { + return { + name: input.name_for_human, + pluginKey: input.name_for_model, + description: input.description_for_human, + icon: input?.logo_url ?? 'https://placehold.co/70x70.png', + // TODO: add support for authentication + isAuthRequired: 'false', + authConfig: [], + }; +} + +async function addOpenAPISpecs(availableTools) { + try { + const specs = (await loadSpecs({})).map(transformSpec); + if (specs.length > 0) { + return [...specs, ...availableTools]; + } + return availableTools; + } catch (error) { + console.log('addOpenAPISpecs error', error); + return availableTools; + } +} + +module.exports = { + transformSpec, + addOpenAPISpecs, +}; diff --git a/api/app/clients/tools/util/addOpenAPISpecs.spec.js b/api/app/clients/tools/util/addOpenAPISpecs.spec.js new file mode 100644 index 00000000000..21ff4eb8cc1 --- /dev/null +++ b/api/app/clients/tools/util/addOpenAPISpecs.spec.js @@ -0,0 +1,76 @@ +const { addOpenAPISpecs, transformSpec } = require('./addOpenAPISpecs'); +const { loadSpecs } = require('./loadSpecs'); +const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin'); + +jest.mock('./loadSpecs'); +jest.mock('../dynamic/OpenAPIPlugin'); + +describe('transformSpec', () => { + it('should transform input spec to a desired format', () => { + const input = { + name_for_human: 'Human Name', + name_for_model: 'Model Name', + description_for_human: 'Human Description', + logo_url: 'https://example.com/logo.png', + }; + + const expectedOutput = { + name: 'Human Name', + pluginKey: 'Model Name', + description: 'Human Description', + icon: 'https://example.com/logo.png', + isAuthRequired: 'false', + authConfig: [], + }; + + expect(transformSpec(input)).toEqual(expectedOutput); + }); + + it('should use default icon if logo_url is not provided', () => { + const input = { + name_for_human: 'Human Name', + name_for_model: 'Model Name', + description_for_human: 'Human Description', + }; + + const expectedOutput = { + name: 'Human Name', + pluginKey: 'Model Name', + description: 'Human Description', + icon: 'https://placehold.co/70x70.png', + isAuthRequired: 'false', + authConfig: [], + }; + + expect(transformSpec(input)).toEqual(expectedOutput); + }); +}); + +describe('addOpenAPISpecs', () => { + it('should add specs to available tools', async () => { + const availableTools = ['Tool1', 'Tool2']; + const specs = [ + { + name_for_human: 'Human Name', + name_for_model: 'Model Name', + description_for_human: 'Human Description', + logo_url: 'https://example.com/logo.png', + }, + ]; + + loadSpecs.mockResolvedValue(specs); + createOpenAPIPlugin.mockReturnValue('Plugin'); + + const result = await addOpenAPISpecs(availableTools); + expect(result).toEqual([...specs.map(transformSpec), ...availableTools]); + }); + + it('should return available tools if specs loading fails', async () => { + const availableTools = ['Tool1', 'Tool2']; + + loadSpecs.mockRejectedValue(new Error('Failed to load specs')); + + const result = await addOpenAPISpecs(availableTools); + expect(result).toEqual(availableTools); + }); +}); diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index 018eb0bcdee..13bf2fe182a 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -16,6 +16,7 @@ const { StableDiffusionAPI, StructuredSD, } = require('../'); +const { loadSpecs } = require('./loadSpecs'); const validateTools = async (user, tools = []) => { try { @@ -80,7 +81,7 @@ const loadTools = async ({ user, model, functions = null, tools = [], options = }; const customConstructors = { - browser: async () => { + 'web-browser': async () => { let openAIApiKey = options.openAIApiKey ?? process.env.OPENAI_API_KEY; openAIApiKey = openAIApiKey === 'user_provided' ? null : openAIApiKey; openAIApiKey = openAIApiKey || (await getUserPluginAuthValue(user, 'OPENAI_API_KEY')); @@ -117,6 +118,17 @@ const loadTools = async ({ user, model, functions = null, tools = [], options = }; const requestedTools = {}; + let specs = null; + if (functions) { + specs = await loadSpecs({ + llm: model, + user, + message: options.message, + map: true, + verbose: options?.debug, + }); + console.dir(specs, { depth: null }); + } const toolOptions = { serpapi: { location: 'Austin,Texas,United States', hl: 'en', gl: 'us' }, @@ -138,6 +150,11 @@ const loadTools = async ({ user, model, functions = null, tools = [], options = continue; } + if (specs && specs[tool]) { + requestedTools[tool] = specs[tool]; + continue; + } + if (toolConstructors[tool]) { const options = toolOptions[tool] || {}; const toolInstance = await loadToolWithAuth( diff --git a/api/app/clients/tools/util/loadSpecs.js b/api/app/clients/tools/util/loadSpecs.js new file mode 100644 index 00000000000..d98e6c645f9 --- /dev/null +++ b/api/app/clients/tools/util/loadSpecs.js @@ -0,0 +1,104 @@ +const fs = require('fs'); +const path = require('path'); +const { z } = require('zod'); +const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin'); + +// The minimum Manifest definition +const ManifestDefinition = z.object({ + schema_version: z.string().optional(), + name_for_human: z.string(), + name_for_model: z.string(), + description_for_human: z.string(), + description_for_model: z.string(), + auth: z.object({}).optional(), + api: z.object({ + // Spec URL or can be the filename of the OpenAPI spec yaml file, + // located in api\app\clients\tools\.well-known\openapi + url: z.string(), + type: z.string().optional(), + is_user_authenticated: z.boolean().nullable().optional(), + has_user_authentication: z.boolean().nullable().optional(), + }), + // use to override any params that the LLM will consistently get wrong + params: z.object({}).optional(), + logo_url: z.string().optional(), + contact_email: z.string().optional(), + legal_info_url: z.string().optional(), +}); + +function validateJson(json, verbose = true) { + try { + return ManifestDefinition.parse(json); + } catch (error) { + if (verbose) { + console.debug('validateJson error', error); + } + return false; + } +} + +// omit the LLM to return the well known jsons as objects +async function loadSpecs({ llm, user, message, map = false, verbose = false }) { + const directoryPath = path.join(__dirname, '..', '.well-known'); + const files = (await fs.promises.readdir(directoryPath)).filter( + (file) => path.extname(file) === '.json', + ); + + const validJsons = []; + const constructorMap = {}; + + if (verbose) { + console.debug('files', files); + } + + for (const file of files) { + if (path.extname(file) === '.json') { + const filePath = path.join(directoryPath, file); + const fileContent = await fs.promises.readFile(filePath, 'utf8'); + const json = JSON.parse(fileContent); + + if (!validateJson(json)) { + verbose && console.debug('Invalid json', json); + continue; + } + + if (llm && map) { + constructorMap[json.name_for_model] = async () => + await createOpenAPIPlugin({ + data: json, + llm, + message, + user, + verbose, + }); + continue; + } + + if (llm) { + validJsons.push(createOpenAPIPlugin({ data: json, llm, verbose })); + continue; + } + + validJsons.push(json); + } + } + + if (map) { + return constructorMap; + } + + const plugins = (await Promise.all(validJsons)).filter((plugin) => plugin); + + // if (verbose) { + // console.debug('plugins', plugins); + // console.debug(plugins[0].name); + // } + + return plugins; +} + +module.exports = { + loadSpecs, + validateJson, + ManifestDefinition, +}; diff --git a/api/app/clients/tools/util/loadSpecs.spec.js b/api/app/clients/tools/util/loadSpecs.spec.js new file mode 100644 index 00000000000..7b906d86f0c --- /dev/null +++ b/api/app/clients/tools/util/loadSpecs.spec.js @@ -0,0 +1,101 @@ +const fs = require('fs'); +const { validateJson, loadSpecs, ManifestDefinition } = require('./loadSpecs'); +const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin'); + +jest.mock('../dynamic/OpenAPIPlugin'); + +describe('ManifestDefinition', () => { + it('should validate correct json', () => { + const json = { + name_for_human: 'Test', + name_for_model: 'Test', + description_for_human: 'Test', + description_for_model: 'Test', + api: { + url: 'http://test.com', + }, + }; + + expect(() => ManifestDefinition.parse(json)).not.toThrow(); + }); + + it('should not validate incorrect json', () => { + const json = { + name_for_human: 'Test', + name_for_model: 'Test', + description_for_human: 'Test', + description_for_model: 'Test', + api: { + url: 123, // incorrect type + }, + }; + + expect(() => ManifestDefinition.parse(json)).toThrow(); + }); +}); + +describe('validateJson', () => { + it('should return parsed json if valid', () => { + const json = { + name_for_human: 'Test', + name_for_model: 'Test', + description_for_human: 'Test', + description_for_model: 'Test', + api: { + url: 'http://test.com', + }, + }; + + expect(validateJson(json)).toEqual(json); + }); + + it('should return false if json is not valid', () => { + const json = { + name_for_human: 'Test', + name_for_model: 'Test', + description_for_human: 'Test', + description_for_model: 'Test', + api: { + url: 123, // incorrect type + }, + }; + + expect(validateJson(json)).toEqual(false); + }); +}); + +describe('loadSpecs', () => { + beforeEach(() => { + jest.spyOn(fs.promises, 'readdir').mockResolvedValue(['test.json']); + jest.spyOn(fs.promises, 'readFile').mockResolvedValue( + JSON.stringify({ + name_for_human: 'Test', + name_for_model: 'Test', + description_for_human: 'Test', + description_for_model: 'Test', + api: { + url: 'http://test.com', + }, + }), + ); + createOpenAPIPlugin.mockResolvedValue({}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should return plugins', async () => { + const plugins = await loadSpecs({ llm: true, verbose: false }); + + expect(plugins).toHaveLength(1); + expect(createOpenAPIPlugin).toHaveBeenCalledTimes(1); + }); + + it('should return constructorMap if map is true', async () => { + const plugins = await loadSpecs({ llm: {}, map: true, verbose: false }); + + expect(plugins).toHaveProperty('Test'); + expect(createOpenAPIPlugin).not.toHaveBeenCalled(); + }); +}); diff --git a/api/lib/db/migrateDb.js b/api/lib/db/migrateDb.js index d25a3022125..a3469996af9 100644 --- a/api/lib/db/migrateDb.js +++ b/api/lib/db/migrateDb.js @@ -6,7 +6,9 @@ const migrateToStrictFollowParentMessageIdChain = async () => { try { const conversations = await Conversation.find({ endpoint: null, model: null }).exec(); - if (!conversations || conversations.length === 0) return { noNeed: true }; + if (!conversations || conversations.length === 0) { + return { noNeed: true }; + } console.log('Migration: To strict follow the parentMessageId chain.'); @@ -64,7 +66,9 @@ const migrateToSupportBetterCustomization = async () => { try { const conversations = await Conversation.find({ endpoint: null }).exec(); - if (!conversations || conversations.length === 0) return { noNeed: true }; + if (!conversations || conversations.length === 0) { + return { noNeed: true }; + } console.log('Migration: To support better customization.'); @@ -112,7 +116,9 @@ async function migrateDb() { const isMigrated = !!ret.find((element) => !element?.noNeed); - if (!isMigrated) console.log('[Migrate] Nothing to migrate'); + if (!isMigrated) { + console.log('[Migrate] Nothing to migrate'); + } } module.exports = migrateDb; diff --git a/api/lib/parse/citeText.js b/api/lib/parse/citeText.js index 8f9cbe9dd8d..8fc1cea8b4f 100644 --- a/api/lib/parse/citeText.js +++ b/api/lib/parse/citeText.js @@ -3,7 +3,9 @@ const citationRegex = /\[\^\d+?\^\]/g; const citeText = (res, noLinks = false) => { let result = res.text || res; const citations = Array.from(new Set(result.match(citationRegex))); - if (citations?.length === 0) return result; + if (citations?.length === 0) { + return result; + } if (noLinks) { citations.forEach((citation) => { @@ -16,7 +18,9 @@ const citeText = (res, noLinks = false) => { } let sources = res.details.sourceAttributions; - if (sources?.length === 0) return result; + if (sources?.length === 0) { + return result; + } sources = sources.map((source) => source.seeMoreUrl); citations.forEach((citation) => { diff --git a/api/lib/parse/getCitations.js b/api/lib/parse/getCitations.js index f8c4d4a8ae3..f99363d1453 100644 --- a/api/lib/parse/getCitations.js +++ b/api/lib/parse/getCitations.js @@ -4,9 +4,13 @@ const regex = / \[.*?]\(.*?\)/g; const getCitations = (res) => { const adaptiveCards = res.details.adaptiveCards; const textBlocks = adaptiveCards && adaptiveCards[0].body; - if (!textBlocks) return ''; + if (!textBlocks) { + return ''; + } let links = textBlocks[textBlocks.length - 1]?.text.match(regex); - if (links?.length === 0 || !links) return ''; + if (links?.length === 0 || !links) { + return ''; + } links = links.map((link) => link.trim()); return links.join('\n - '); }; diff --git a/api/lib/utils/misc.js b/api/lib/utils/misc.js index c7bf9e39e92..1abcff9da6c 100644 --- a/api/lib/utils/misc.js +++ b/api/lib/utils/misc.js @@ -4,7 +4,9 @@ const cleanUpPrimaryKeyValue = (value) => { }; function replaceSup(text) { - if (!text.includes('')) return text; + if (!text.includes('')) { + return text; + } const replacedText = text.replace(//g, '^').replace(/\s+<\/sup>/g, '^'); return replacedText; } diff --git a/api/models/Message.js b/api/models/Message.js index 37235bebe6e..837d3dee67e 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -56,11 +56,7 @@ module.exports = { async updateMessage(message) { try { const { messageId, ...update } = message; - const updatedMessage = await Message.findOneAndUpdate( - { messageId }, - update, - { new: true }, - ); + const updatedMessage = await Message.findOneAndUpdate({ messageId }, update, { new: true }); if (!updatedMessage) { throw new Error('Message not found.'); diff --git a/api/models/User.js b/api/models/User.js index 8421e3e909b..e6ea9ce75ca 100644 --- a/api/models/User.js +++ b/api/models/User.js @@ -145,7 +145,9 @@ userSchema.methods.generateRefreshToken = function () { userSchema.methods.comparePassword = function (candidatePassword, callback) { bcrypt.compare(candidatePassword, this.password, (err, isMatch) => { - if (err) return callback(err); + if (err) { + return callback(err); + } callback(null, isMatch); }); }; @@ -153,8 +155,11 @@ userSchema.methods.comparePassword = function (candidatePassword, callback) { module.exports.hashPassword = async (password) => { const hashedPassword = await new Promise((resolve, reject) => { bcrypt.hash(password, 10, function (err, hash) { - if (err) reject(err); - else resolve(hash); + if (err) { + reject(err); + } else { + resolve(hash); + } }); }); diff --git a/api/models/index.js b/api/models/index.js index b09055d01d0..a42d2c177f3 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -1,4 +1,10 @@ -const { getMessages, saveMessage, updateMessage, deleteMessagesSince, deleteMessages } = require('./Message'); +const { + getMessages, + saveMessage, + updateMessage, + deleteMessagesSince, + deleteMessages, +} = require('./Message'); const { getConvoTitle, getConvo, saveConvo } = require('./Conversation'); const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset'); diff --git a/api/models/plugins/mongoMeili.js b/api/models/plugins/mongoMeili.js index 68b101fd845..3325d84fc6a 100644 --- a/api/models/plugins/mongoMeili.js +++ b/api/models/plugins/mongoMeili.js @@ -8,7 +8,9 @@ const meiliEnabled = process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY && s const validateOptions = function (options) { const requiredKeys = ['host', 'apiKey', 'indexName']; requiredKeys.forEach((key) => { - if (!options[key]) throw new Error(`Missing mongoMeili Option: ${key}`); + if (!options[key]) { + throw new Error(`Missing mongoMeili Option: ${key}`); + } }); }; @@ -96,12 +98,12 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute if (object.conversationId && object.conversationId.includes('|')) { object.conversationId = object.conversationId.replace(/\|/g, '--'); } - return object + return object; } // Push new document to Meili async addObjectToMeili() { - const object = this.preprocessObjectForIndex() + const object = this.preprocessObjectForIndex(); try { // console.log('Adding document to Meili', object); await index.addDocuments([object]); @@ -228,7 +230,9 @@ module.exports = function mongoMeili(schema, options) { return next(); } catch (error) { if (meiliEnabled) { - console.log('[Meilisearch] There was an issue deleting conversation indexes upon deletion, next startup may be slow due to syncing'); + console.log( + '[Meilisearch] There was an issue deleting conversation indexes upon deletion, next startup may be slow due to syncing', + ); console.error(error); } return next(); diff --git a/api/models/schema/defaults.js b/api/models/schema/defaults.js index 13c2fd0d4a5..92e064480e4 100644 --- a/api/models/schema/defaults.js +++ b/api/models/schema/defaults.js @@ -155,4 +155,4 @@ const agentOptions = { module.exports = { conversationPreset, agentOptions, -}; \ No newline at end of file +}; diff --git a/api/models/schema/pluginAuthSchema.js b/api/models/schema/pluginAuthSchema.js index 296f9033372..4b4251dda37 100644 --- a/api/models/schema/pluginAuthSchema.js +++ b/api/models/schema/pluginAuthSchema.js @@ -23,4 +23,4 @@ const pluginAuthSchema = mongoose.Schema( const PluginAuth = mongoose.models.Plugin || mongoose.model('PluginAuth', pluginAuthSchema); -module.exports = PluginAuth; \ No newline at end of file +module.exports = PluginAuth; diff --git a/api/package.json b/api/package.json index 86eb78bd201..7371c972183 100644 --- a/api/package.json +++ b/api/package.json @@ -43,7 +43,7 @@ "jsonwebtoken": "^9.0.0", "keyv": "^4.5.2", "keyv-file": "^0.2.0", - "langchain": "^0.0.103", + "langchain": "^0.0.109", "lodash": "^4.17.21", "meilisearch": "^0.33.0", "mongoose": "^7.1.1", diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js index 442af996efe..34631e7442d 100644 --- a/api/server/controllers/AuthController.js +++ b/api/server/controllers/AuthController.js @@ -1,8 +1,4 @@ -const { - registerUser, - requestPasswordReset, - resetPassword, -} = require('../services/auth.service'); +const { registerUser, requestPasswordReset, resetPassword } = require('../services/auth.service'); const isProduction = process.env.NODE_ENV === 'production'; diff --git a/api/server/controllers/ErrorController.js b/api/server/controllers/ErrorController.js index 1d32f306a52..cdfd5b97a61 100644 --- a/api/server/controllers/ErrorController.js +++ b/api/server/controllers/ErrorController.js @@ -25,8 +25,12 @@ const handleValidationError = (err, res) => { module.exports = (err, req, res, next) => { try { console.log('congrats you hit the error middleware'); - if (err.name === 'ValidationError') return (err = handleValidationError(err, res)); - if (err.code && err.code == 11000) return (err = handleDuplicateKeyError(err, res)); + if (err.name === 'ValidationError') { + return (err = handleValidationError(err, res)); + } + if (err.code && err.code == 11000) { + return (err = handleDuplicateKeyError(err, res)); + } } catch (err) { res.status(500).send('An unknown error occurred.'); } diff --git a/api/server/controllers/PluginController.js b/api/server/controllers/PluginController.js index 1f6d35064be..304c089657a 100644 --- a/api/server/controllers/PluginController.js +++ b/api/server/controllers/PluginController.js @@ -1,6 +1,6 @@ -// const { getAvailableToolsService } = require('../services/PluginService'); -const fs = require('fs'); +const { promises: fs } = require('fs'); const path = require('path'); +const { addOpenAPISpecs } = require('../../app/clients/tools/util/addOpenAPISpecs'); const filterUniquePlugins = (plugins) => { const seen = new Set(); @@ -27,26 +27,22 @@ const isPluginAuthenticated = (plugin) => { const getAvailablePluginsController = async (req, res) => { try { - fs.readFile( + const manifestFile = await fs.readFile( path.join(__dirname, '..', '..', 'app', 'clients', 'tools', 'manifest.json'), 'utf8', - (err, data) => { - if (err) { - res.status(500).json({ message: err.message }); - } else { - const jsonData = JSON.parse(data); - const uniquePlugins = filterUniquePlugins(jsonData); - const authenticatedPlugins = uniquePlugins.map((plugin) => { - if (isPluginAuthenticated(plugin)) { - return { ...plugin, authenticated: true }; - } else { - return plugin; - } - }); - res.status(200).json(authenticatedPlugins); - } - }, ); + + const jsonData = JSON.parse(manifestFile); + const uniquePlugins = filterUniquePlugins(jsonData); + const authenticatedPlugins = uniquePlugins.map((plugin) => { + if (isPluginAuthenticated(plugin)) { + return { ...plugin, authenticated: true }; + } else { + return plugin; + } + }); + const plugins = await addOpenAPISpecs(authenticatedPlugins); + res.status(200).json(plugins); } catch (error) { res.status(500).json({ message: error.message }); } diff --git a/api/server/controllers/auth/LoginController.js b/api/server/controllers/auth/LoginController.js index dddcadf4773..0c7cf271f37 100644 --- a/api/server/controllers/auth/LoginController.js +++ b/api/server/controllers/auth/LoginController.js @@ -2,12 +2,11 @@ const User = require('../../../models/User'); const loginController = async (req, res) => { try { - const user = await User.findById( - req.user._id, - ); + const user = await User.findById(req.user._id); // If user doesn't exist, return error - if (!user) { // typeof user !== User) { // this doesn't seem to resolve the User type ?? + if (!user) { + // typeof user !== User) { // this doesn't seem to resolve the User type ?? return res.status(400).json({ message: 'Invalid credentials' }); } @@ -15,15 +14,11 @@ const loginController = async (req, res) => { const expires = eval(process.env.SESSION_EXPIRY); // Add token to cookie - res.cookie( - 'token', - token, - { - expires: new Date(Date.now() + expires), - httpOnly: false, - secure: process.env.NODE_ENV === 'production', - }, - ); + res.cookie('token', token, { + expires: new Date(Date.now() + expires), + httpOnly: false, + secure: process.env.NODE_ENV === 'production', + }); return res.status(200).send({ token, user }); } catch (err) { @@ -36,4 +31,4 @@ const loginController = async (req, res) => { module.exports = { loginController, -}; \ No newline at end of file +}; diff --git a/api/server/controllers/auth/LogoutController.js b/api/server/controllers/auth/LogoutController.js index c4561c0a418..29bc70b7b00 100644 --- a/api/server/controllers/auth/LogoutController.js +++ b/api/server/controllers/auth/LogoutController.js @@ -9,7 +9,6 @@ const logoutController = async (req, res) => { res.clearCookie('token'); res.clearCookie('refreshToken'); return res.status(status).send({ message }); - } catch (err) { console.log(err); return res.status(500).json({ message: err.message }); @@ -18,4 +17,4 @@ const logoutController = async (req, res) => { module.exports = { logoutController, -}; \ No newline at end of file +}; diff --git a/api/server/index.js b/api/server/index.js index aa9bcef9807..c574432a18b 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -33,7 +33,9 @@ config.validate(); // Validate the config app.use(cors()); if (!process.env.ALLOW_SOCIAL_LOGIN) { - console.warn('Social logins are disabled. Set Envrionment Variable "ALLOW_SOCIAL_LOGIN" to true to enable them.') + console.warn( + 'Social logins are disabled. Set Envrionment Variable "ALLOW_SOCIAL_LOGIN" to true to enable them.', + ); } // OAUTH @@ -52,14 +54,20 @@ config.validate(); // Validate the config if (process.env.DISCORD_CLIENT_ID && process.env.DISCORD_CLIENT_SECRET) { require('../strategies/discordStrategy'); } - if (process.env.OPENID_CLIENT_ID && process.env.OPENID_CLIENT_SECRET && - process.env.OPENID_ISSUER && process.env.OPENID_SCOPE && - process.env.OPENID_SESSION_SECRET) { - app.use(session({ - secret: process.env.OPENID_SESSION_SECRET, - resave: false, - saveUninitialized: false, - })); + if ( + process.env.OPENID_CLIENT_ID && + process.env.OPENID_CLIENT_SECRET && + process.env.OPENID_ISSUER && + process.env.OPENID_SCOPE && + process.env.OPENID_SESSION_SECRET + ) { + app.use( + session({ + secret: process.env.OPENID_SESSION_SECRET, + resave: false, + saveUninitialized: false, + }), + ); app.use(passport.session()); require('../strategies/openidStrategy'); } @@ -84,12 +92,13 @@ config.validate(); // Validate the config }); app.listen(port, host, () => { - if (host == '0.0.0.0') + if (host == '0.0.0.0') { console.log( `Server listening on all interface at port ${port}. Use http://localhost:${port} to access it`, ); - else + } else { console.log(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`); + } }); })(); diff --git a/api/server/routes/__tests__/config.spec.js b/api/server/routes/__tests__/config.spec.js index c3f0ba77825..87ce05af016 100644 --- a/api/server/routes/__tests__/config.spec.js +++ b/api/server/routes/__tests__/config.spec.js @@ -31,16 +31,16 @@ describe.skip('GET /', () => { process.env.APP_TITLE = 'Test Title'; process.env.GOOGLE_CLIENT_ID = 'Test Google Client Id'; process.env.GOOGLE_CLIENT_SECRET = 'Test Google Client Secret'; - process.env.OPENID_CLIENT_ID= 'Test OpenID Id'; - process.env.OPENID_CLIENT_SECRET= 'Test OpenID Secret'; - process.env.OPENID_ISSUER= 'Test OpenID Issuer'; - process.env.OPENID_SESSION_SECRET= 'Test Secret'; - process.env.OPENID_BUTTON_LABEL= 'Test OpenID'; - process.env.OPENID_AUTH_URL= 'http://test-server.com'; + process.env.OPENID_CLIENT_ID = 'Test OpenID Id'; + process.env.OPENID_CLIENT_SECRET = 'Test OpenID Secret'; + process.env.OPENID_ISSUER = 'Test OpenID Issuer'; + process.env.OPENID_SESSION_SECRET = 'Test Secret'; + process.env.OPENID_BUTTON_LABEL = 'Test OpenID'; + process.env.OPENID_AUTH_URL = 'http://test-server.com'; process.env.GITHUB_CLIENT_ID = 'Test Github client Id'; - process.env.GITHUB_CLIENT_SECRET= 'Test Github client Secret'; + process.env.GITHUB_CLIENT_SECRET = 'Test Github client Secret'; process.env.DISCORD_CLIENT_ID = 'Test Discord client Id'; - process.env.DISCORD_CLIENT_SECRET= 'Test Discord client Secret'; + process.env.DISCORD_CLIENT_SECRET = 'Test Discord client Secret'; process.env.DOMAIN_SERVER = 'http://test-server.com'; process.env.ALLOW_REGISTRATION = 'true'; process.env.ALLOW_SOCIAL_LOGIN = 'true'; diff --git a/api/server/routes/ask/anthropic.js b/api/server/routes/ask/anthropic.js index c50aa97b32f..58f4aba8e43 100644 --- a/api/server/routes/ask/anthropic.js +++ b/api/server/routes/ask/anthropic.js @@ -15,8 +15,12 @@ router.post('/abort', requireJwtAuth, async (req, res) => { router.post('/', requireJwtAuth, async (req, res) => { const { endpoint, text, parentMessageId, conversationId: oldConversationId } = req.body; - if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' }); - if (endpoint !== 'anthropic') return handleError(res, { text: 'Illegal request' }); + if (text.length === 0) { + return handleError(res, { text: 'Prompt empty or too short' }); + } + if (endpoint !== 'anthropic') { + return handleError(res, { text: 'Illegal request' }); + } const endpointOption = { promptPrefix: req.body?.promptPrefix ?? null, @@ -117,7 +121,7 @@ const ask = async ({ text, endpointOption, parentMessageId = null, conversationI const onStart = (userMessage) => { sendMessage(res, { message: userMessage, created: true }); abortControllers.set(userMessage.conversationId, { abortController, ...endpointOption }); - } + }; const client = new AnthropicClient(endpointOption.token); diff --git a/api/server/routes/ask/askChatGPTBrowser.js b/api/server/routes/ask/askChatGPTBrowser.js index 9e4e8aacefb..576f5810810 100644 --- a/api/server/routes/ask/askChatGPTBrowser.js +++ b/api/server/routes/ask/askChatGPTBrowser.js @@ -15,8 +15,12 @@ router.post('/', requireJwtAuth, async (req, res) => { parentMessageId, conversationId: oldConversationId, } = req.body; - if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' }); - if (endpoint !== 'chatGPTBrowser') return handleError(res, { text: 'Illegal request' }); + if (text.length === 0) { + return handleError(res, { text: 'Prompt empty or too short' }); + } + if (endpoint !== 'chatGPTBrowser') { + return handleError(res, { text: 'Illegal request' }); + } // build user message const conversationId = oldConversationId || crypto.randomUUID(); @@ -167,7 +171,7 @@ const ask = async ({ // First update conversationId if needed let conversationUpdate = { conversationId: newConversationId, endpoint: 'chatGPTBrowser' }; - if (conversationId != newConversationId) + if (conversationId != newConversationId) { if (isNewConversation) { // change the conversationId to new one conversationUpdate = { @@ -182,6 +186,7 @@ const ask = async ({ ...endpointOption, }; } + } await saveConvo(req.user.id, conversationUpdate); conversationId = newConversationId; @@ -191,12 +196,13 @@ const ask = async ({ userMessage.messageId = newUserMassageId; // If response has parentMessageId, the fake userMessage.messageId should be updated to the real one. - if (!overrideParentMessageId) + if (!overrideParentMessageId) { await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMassageId, }); + } userMessageId = newUserMassageId; sendMessage(res, { diff --git a/api/server/routes/ask/bingAI.js b/api/server/routes/ask/bingAI.js index f80a08c371d..e03529527b8 100644 --- a/api/server/routes/ask/bingAI.js +++ b/api/server/routes/ask/bingAI.js @@ -15,8 +15,12 @@ router.post('/', requireJwtAuth, async (req, res) => { parentMessageId, conversationId: oldConversationId, } = req.body; - if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' }); - if (endpoint !== 'bingAI') return handleError(res, { text: 'Illegal request' }); + if (text.length === 0) { + return handleError(res, { text: 'Prompt empty or too short' }); + } + if (endpoint !== 'bingAI') { + return handleError(res, { text: 'Illegal request' }); + } // build user message const conversationId = oldConversationId || crypto.randomUUID(); @@ -34,7 +38,7 @@ router.post('/', requireJwtAuth, async (req, res) => { // build endpoint option let endpointOption = {}; - if (req.body?.jailbreak) + if (req.body?.jailbreak) { endpointOption = { jailbreak: req.body?.jailbreak ?? false, jailbreakConversationId: req.body?.jailbreakConversationId ?? null, @@ -43,7 +47,7 @@ router.post('/', requireJwtAuth, async (req, res) => { toneStyle: req.body?.toneStyle ?? 'creative', token: req.body?.token ?? null, }; - else + } else { endpointOption = { jailbreak: req.body?.jailbreak ?? false, systemMessage: req.body?.systemMessage ?? null, @@ -54,6 +58,7 @@ router.post('/', requireJwtAuth, async (req, res) => { toneStyle: req.body?.toneStyle ?? 'creative', token: req.body?.token ?? null, }; + } console.log('ask log', { userMessage, @@ -106,7 +111,9 @@ const ask = async ({ 'X-Accel-Buffering': 'no', }); - if (preSendRequest) sendMessage(res, { message: userMessage, created: true }); + if (preSendRequest) { + sendMessage(res, { message: userMessage, created: true }); + } let lastSavedTimestamp = 0; const { onProgress: progressCallback, getPartialText } = createOnProgress({ @@ -207,12 +214,13 @@ const ask = async ({ userMessage.messageId = newUserMessageId; // If response has parentMessageId, the fake userMessage.messageId should be updated to the real one. - if (!overrideParentMessageId) + if (!overrideParentMessageId) { await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMessageId, }); + } userMessageId = newUserMessageId; sendMessage(res, { diff --git a/api/server/routes/ask/google.js b/api/server/routes/ask/google.js index 5d5a163766b..f3d25cbcd4a 100644 --- a/api/server/routes/ask/google.js +++ b/api/server/routes/ask/google.js @@ -9,8 +9,12 @@ const requireJwtAuth = require('../../../middleware/requireJwtAuth'); router.post('/', requireJwtAuth, async (req, res) => { const { endpoint, text, parentMessageId, conversationId: oldConversationId } = req.body; - if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' }); - if (endpoint !== 'google') return handleError(res, { text: 'Illegal request' }); + if (text.length === 0) { + return handleError(res, { text: 'Prompt empty or too short' }); + } + if (endpoint !== 'google') { + return handleError(res, { text: 'Illegal request' }); + } // build endpoint option const endpointOption = { diff --git a/api/server/routes/ask/gptPlugins.js b/api/server/routes/ask/gptPlugins.js index c54129f477a..c4f8a3fc24c 100644 --- a/api/server/routes/ask/gptPlugins.js +++ b/api/server/routes/ask/gptPlugins.js @@ -20,8 +20,12 @@ router.post('/abort', requireJwtAuth, async (req, res) => { router.post('/', requireJwtAuth, async (req, res) => { const { endpoint, text, parentMessageId, conversationId } = req.body; - if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' }); - if (endpoint !== 'gptPlugins') return handleError(res, { text: 'Illegal request' }); + if (text.length === 0) { + return handleError(res, { text: 'Prompt empty or too short' }); + } + if (endpoint !== 'gptPlugins') { + return handleError(res, { text: 'Illegal request' }); + } const agentOptions = req.body?.agentOptions ?? { agent: 'functions', @@ -67,7 +71,15 @@ router.post('/', requireJwtAuth, async (req, res) => { }); }); -const ask = async ({ text, endpoint, endpointOption, parentMessageId = null, conversationId, req, res }) => { +const ask = async ({ + text, + endpoint, + endpointOption, + parentMessageId = null, + conversationId, + req, + res, +}) => { res.writeHead(200, { Connection: 'keep-alive', 'Content-Type': 'text/event-stream', @@ -100,7 +112,11 @@ const ask = async ({ text, endpoint, endpointOption, parentMessageId = null, con } }; - const { onProgress: progressCallback, sendIntermediateMessage, getPartialText } = createOnProgress({ + const { + onProgress: progressCallback, + sendIntermediateMessage, + getPartialText, + } = createOnProgress({ onProgress: ({ text: partialText }) => { const currentTimestamp = Date.now(); @@ -156,7 +172,7 @@ const ask = async ({ text, endpoint, endpointOption, parentMessageId = null, con const onStart = (userMessage) => { sendMessage(res, { message: userMessage, created: true }); abortControllers.set(userMessage.conversationId, { abortController, ...endpointOption }); - } + }; endpointOption.tools = await validateTools(user, endpointOption.tools); const clientOptions = { @@ -179,11 +195,13 @@ const ask = async ({ text, endpoint, endpointOption, parentMessageId = null, con } const chatAgent = new PluginsClient(openAIApiKey, clientOptions); - const onAgentAction = (action) => { + const onAgentAction = (action, start = false) => { const formattedAction = formatAction(action); plugin.inputs.push(formattedAction); plugin.latest = formattedAction.plugin; - saveMessage(userMessage); + if (!start) { + saveMessage(userMessage); + } sendIntermediateMessage(res, { plugin }); // console.log('PLUGIN ACTION', formattedAction); }; diff --git a/api/server/routes/ask/handlers.js b/api/server/routes/ask/handlers.js index c99432a16c3..d917c65ca4a 100644 --- a/api/server/routes/ask/handlers.js +++ b/api/server/routes/ask/handlers.js @@ -61,7 +61,12 @@ const createOnProgress = ({ onProgress: _onProgress }) => { }; const sendIntermediateMessage = (res, payload) => { - sendMessage(res, { text: tokens?.length === 0 ? cursor : tokens, message: true, initial: i === 0, ...payload }); + sendMessage(res, { + text: tokens?.length === 0 ? cursor : tokens, + message: true, + initial: i === 0, + ...payload, + }); i++; }; @@ -92,7 +97,7 @@ const handleText = async (response, bing = false) => { }; const isObject = (item) => item && typeof item === 'object' && !Array.isArray(item); -const getString = (input) => isObject(input) ? JSON.stringify(input) : input ; +const getString = (input) => (isObject(input) ? JSON.stringify(input) : input); function formatSteps(steps) { let output = ''; @@ -117,20 +122,8 @@ function formatSteps(steps) { } function formatAction(action) { - const capitalizeWords = (input) => { - if (input === 'dall-e') { - return 'DALL-E'; - } - - return input - .replace(/-/g, ' ') - .split(' ') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - }; - const formattedAction = { - plugin: capitalizeWords(action.tool) || action.tool, + plugin: action.tool, input: getString(action.toolInput), thought: action.log.includes('Thought: ') ? action.log.split('\n')[0].replace('Thought: ', '') @@ -162,4 +155,4 @@ module.exports = { handleText, formatSteps, formatAction, -}; \ No newline at end of file +}; diff --git a/api/server/routes/ask/openAI.js b/api/server/routes/ask/openAI.js index 03795423d12..608aca2e3f1 100644 --- a/api/server/routes/ask/openAI.js +++ b/api/server/routes/ask/openAI.js @@ -3,11 +3,7 @@ const router = express.Router(); const { titleConvo, OpenAIClient } = require('../../../app'); const { getAzureCredentials, abortMessage } = require('../../../utils'); const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models'); -const { - handleError, - sendMessage, - createOnProgress, -} = require('./handlers'); +const { handleError, sendMessage, createOnProgress } = require('./handlers'); const requireJwtAuth = require('../../../middleware/requireJwtAuth'); const abortControllers = new Map(); @@ -18,9 +14,13 @@ router.post('/abort', requireJwtAuth, async (req, res) => { router.post('/', requireJwtAuth, async (req, res) => { const { endpoint, text, parentMessageId, conversationId } = req.body; - if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' }); + if (text.length === 0) { + return handleError(res, { text: 'Prompt empty or too short' }); + } const isOpenAI = endpoint === 'openAI' || endpoint === 'azureOpenAI'; - if (!isOpenAI) return handleError(res, { text: 'Illegal request' }); + if (!isOpenAI) { + return handleError(res, { text: 'Illegal request' }); + } // build endpoint option const endpointOption = { @@ -50,7 +50,15 @@ router.post('/', requireJwtAuth, async (req, res) => { }); }); -const ask = async ({ text, endpointOption, parentMessageId = null, endpoint, conversationId, req, res }) => { +const ask = async ({ + text, + endpointOption, + parentMessageId = null, + endpoint, + conversationId, + req, + res, +}) => { res.writeHead(200, { Connection: 'keep-alive', 'Content-Type': 'text/event-stream', @@ -166,7 +174,11 @@ const ask = async ({ text, endpointOption, parentMessageId = null, endpoint, con response.parentMessageId = overrideParentMessageId; } - console.log('promptTokens, completionTokens:', response.promptTokens, response.completionTokens); + console.log( + 'promptTokens, completionTokens:', + response.promptTokens, + response.completionTokens, + ); await saveMessage(response); sendMessage(res, { diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 3bb04b4140c..cf1611db3a7 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -5,14 +5,16 @@ router.get('/', async function (req, res) { try { const appTitle = process.env.APP_TITLE || 'LibreChat'; const googleLoginEnabled = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET; - const openidLoginEnabled = !!process.env.OPENID_CLIENT_ID - && !!process.env.OPENID_CLIENT_SECRET - && !!process.env.OPENID_ISSUER - && !!process.env.OPENID_SESSION_SECRET; + const openidLoginEnabled = + !!process.env.OPENID_CLIENT_ID && + !!process.env.OPENID_CLIENT_SECRET && + !!process.env.OPENID_ISSUER && + !!process.env.OPENID_SESSION_SECRET; const openidLabel = process.env.OPENID_BUTTON_LABEL || 'Login with OpenID'; const openidImageUrl = process.env.OPENID_IMAGE_URL; const githubLoginEnabled = !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET; - const discordLoginEnabled = !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET; + const discordLoginEnabled = + !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET; const serverDomain = process.env.DOMAIN_SERVER || 'http://localhost:3080'; const registrationEnabled = process.env.ALLOW_REGISTRATION === 'true'; const socialLoginEnabled = process.env.ALLOW_SOCIAL_LOGIN === 'true'; @@ -29,7 +31,6 @@ router.get('/', async function (req, res) { registrationEnabled, socialLoginEnabled, }); - } catch (err) { console.error(err); return res.status(500).send({ error: err.message }); diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 5eb56a2df43..29a88de997e 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -13,8 +13,11 @@ router.get('/:conversationId', requireJwtAuth, async (req, res) => { const { conversationId } = req.params; const convo = await getConvo(req.user.id, conversationId); - if (convo) res.status(200).send(convo.toObject()); - else res.status(404).end(); + if (convo) { + res.status(200).send(convo.toObject()); + } else { + res.status(404).end(); + } }); router.post('/clear', requireJwtAuth, async (req, res) => { diff --git a/api/server/routes/endpoints.js b/api/server/routes/endpoints.js index 1b54b77fa35..a556939dd28 100644 --- a/api/server/routes/endpoints.js +++ b/api/server/routes/endpoints.js @@ -1,31 +1,61 @@ const express = require('express'); const router = express.Router(); const { availableTools } = require('../../app/clients/tools'); +const { addOpenAPISpecs } = require('../../app/clients/tools/util/addOpenAPISpecs'); const getOpenAIModels = (opts = { azure: false }) => { - let models = ['gpt-4', 'gpt-4-0613', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k', 'gpt-3.5-turbo-0613', 'gpt-3.5-turbo-0301', 'text-davinci-003' ]; + let models = [ + 'gpt-4', + 'gpt-4-0613', + 'gpt-3.5-turbo', + 'gpt-3.5-turbo-16k', + 'gpt-3.5-turbo-0613', + 'gpt-3.5-turbo-0301', + 'text-davinci-003', + ]; const key = opts.azure ? 'AZURE_OPENAI_MODELS' : 'OPENAI_MODELS'; - if (process.env[key]) models = String(process.env[key]).split(','); + if (process.env[key]) { + models = String(process.env[key]).split(','); + } return models; }; const getChatGPTBrowserModels = () => { let models = ['text-davinci-002-render-sha', 'gpt-4']; - if (process.env.CHATGPT_MODELS) models = String(process.env.CHATGPT_MODELS).split(','); + if (process.env.CHATGPT_MODELS) { + models = String(process.env.CHATGPT_MODELS).split(','); + } return models; }; const getAnthropicModels = () => { - let models = ['claude-1', 'claude-1-100k', 'claude-instant-1', 'claude-instant-1-100k', 'claude-2']; - if (process.env.ANTHROPIC_MODELS) models = String(process.env.ANTHROPIC_MODELS).split(','); + let models = [ + 'claude-1', + 'claude-1-100k', + 'claude-instant-1', + 'claude-instant-1-100k', + 'claude-2', + ]; + if (process.env.ANTHROPIC_MODELS) { + models = String(process.env.ANTHROPIC_MODELS).split(','); + } return models; }; const getPluginModels = () => { - let models = ['gpt-4', 'gpt-4-0613', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k', 'gpt-3.5-turbo-0613', 'gpt-3.5-turbo-0301']; - if (process.env.PLUGIN_MODELS) models = String(process.env.PLUGIN_MODELS).split(','); + let models = [ + 'gpt-4', + 'gpt-4-0613', + 'gpt-3.5-turbo', + 'gpt-3.5-turbo-16k', + 'gpt-3.5-turbo-0613', + 'gpt-3.5-turbo-0301', + ]; + if (process.env.PLUGIN_MODELS) { + models = String(process.env.PLUGIN_MODELS).split(','); + } return models; }; @@ -50,22 +80,42 @@ router.get('/', async function (req, res) { } } + const tools = await addOpenAPISpecs(availableTools); + function transformToolsToMap(tools) { + return tools.reduce((map, obj) => { + map[obj.pluginKey] = obj.name; + return map; + }, {}); + } + const plugins = transformToolsToMap(tools); + const google = key || palmUser ? { userProvide: palmUser, availableModels: ['chat-bison', 'text-bison', 'codechat-bison'] } : false; const openAIApiKey = process.env.OPENAI_API_KEY; const azureOpenAIApiKey = process.env.AZURE_API_KEY; - const userProvidedOpenAI = openAIApiKey ? openAIApiKey === 'user_provided' : azureOpenAIApiKey === 'user_provided'; + const userProvidedOpenAI = openAIApiKey + ? openAIApiKey === 'user_provided' + : azureOpenAIApiKey === 'user_provided'; const openAI = openAIApiKey ? { availableModels: getOpenAIModels(), userProvide: openAIApiKey === 'user_provided' } : false; const azureOpenAI = azureOpenAIApiKey - ? { availableModels: getOpenAIModels({ azure: true }), userProvide: azureOpenAIApiKey === 'user_provided' } - : false; - const gptPlugins = openAIApiKey || azureOpenAIApiKey - ? { availableModels: getPluginModels(), availableTools, availableAgents: ['classic', 'functions'], userProvide: userProvidedOpenAI } + ? { + availableModels: getOpenAIModels({ azure: true }), + userProvide: azureOpenAIApiKey === 'user_provided', + } : false; + const gptPlugins = + openAIApiKey || azureOpenAIApiKey + ? { + availableModels: getPluginModels(), + plugins, + availableAgents: ['classic', 'functions'], + userProvide: userProvidedOpenAI, + } + : false; const bingAI = process.env.BINGAI_TOKEN ? { userProvide: process.env.BINGAI_TOKEN == 'user_provided' } : false; @@ -82,7 +132,9 @@ router.get('/', async function (req, res) { } : false; - res.send(JSON.stringify({ azureOpenAI, openAI, google, bingAI, chatGPTBrowser, gptPlugins, anthropic })); + res.send( + JSON.stringify({ azureOpenAI, openAI, google, bingAI, chatGPTBrowser, gptPlugins, anthropic }), + ); }); module.exports = { router, getOpenAIModels, getChatGPTBrowserModels }; diff --git a/api/server/routes/presets.js b/api/server/routes/presets.js index 59a1d6051d3..5ae708d85dd 100644 --- a/api/server/routes/presets.js +++ b/api/server/routes/presets.js @@ -33,7 +33,9 @@ router.post('/delete', requireJwtAuth, async (req, res) => { let filter = {}; const { presetId } = req.body.arg || {}; - if (presetId) filter = { presetId }; + if (presetId) { + filter = { presetId }; + } console.log('delete preset filter', filter); diff --git a/api/server/services/auth.service.js b/api/server/services/auth.service.js index 71bef1a40fd..2467479fc51 100644 --- a/api/server/services/auth.service.js +++ b/api/server/services/auth.service.js @@ -110,7 +110,9 @@ const requestPasswordReset = async (email) => { } let token = await Token.findOne({ userId: user._id }); - if (token) await token.deleteOne(); + if (token) { + await token.deleteOne(); + } let resetToken = crypto.randomBytes(32).toString('hex'); const hash = await bcrypt.hashSync(resetToken, 10); diff --git a/api/strategies/openidStrategy.js b/api/strategies/openidStrategy.js index 6d2511b4c62..cf49fa24a10 100644 --- a/api/strategies/openidStrategy.js +++ b/api/strategies/openidStrategy.js @@ -19,7 +19,7 @@ const downloadImage = async (url, imagePath, accessToken) => { try { const response = await axios.get(url, { headers: { - 'Authorization': `Bearer ${accessToken}`, + Authorization: `Bearer ${accessToken}`, }, responseType: 'arraybuffer', }); @@ -37,7 +37,7 @@ const downloadImage = async (url, imagePath, accessToken) => { }; Issuer.discover(process.env.OPENID_ISSUER) - .then(issuer => { + .then((issuer) => { const client = new issuer.Client({ client_id: process.env.OPENID_CLIENT_ID, client_secret: process.env.OPENID_CLIENT_SECRET, @@ -96,9 +96,22 @@ Issuer.discover(process.env.OPENID_ISSUER) fileName = userinfo.sub + '.png'; } - const imagePath = path.join(__dirname, '..', '..', 'client', 'public', 'images', 'openid', fileName); - - const imagePathOrEmpty = await downloadImage(imageUrl, imagePath, tokenset.access_token); + const imagePath = path.join( + __dirname, + '..', + '..', + 'client', + 'public', + 'images', + 'openid', + fileName, + ); + + const imagePathOrEmpty = await downloadImage( + imageUrl, + imagePath, + tokenset.access_token, + ); user.avatar = imagePathOrEmpty; } else { @@ -115,8 +128,7 @@ Issuer.discover(process.env.OPENID_ISSUER) ); passport.use('openid', openidLogin); - }) - .catch(err => { + .catch((err) => { console.error(err); }); diff --git a/api/utils/LoggingSystem.js b/api/utils/LoggingSystem.js index fdb72851336..d0e78821f5a 100644 --- a/api/utils/LoggingSystem.js +++ b/api/utils/LoggingSystem.js @@ -68,45 +68,65 @@ module.exports = { setLevel: (l) => (level = l), log: { trace: (msg) => { - if (level <= levels.TRACE) return; + if (level <= levels.TRACE) { + return; + } logger.trace(msg); }, debug: (msg) => { - if (level <= levels.DEBUG) return; + if (level <= levels.DEBUG) { + return; + } logger.debug(msg); }, info: (msg) => { - if (level <= levels.INFO) return; + if (level <= levels.INFO) { + return; + } logger.info(msg); }, warn: (msg) => { - if (level <= levels.WARN) return; + if (level <= levels.WARN) { + return; + } logger.warn(msg); }, error: (msg) => { - if (level <= levels.ERROR) return; + if (level <= levels.ERROR) { + return; + } logger.error(msg); }, fatal: (msg) => { - if (level <= levels.FATAL) return; + if (level <= levels.FATAL) { + return; + } logger.fatal(msg); }, // Custom loggers parameters: (parameters) => { - if (level <= levels.TRACE) return; + if (level <= levels.TRACE) { + return; + } logger.debug({ parameters }, 'Function Parameters'); }, functionName: (name) => { - if (level <= levels.TRACE) return; + if (level <= levels.TRACE) { + return; + } logger.debug(`EXECUTING: ${name}`); }, flow: (flow) => { - if (level <= levels.INFO) return; + if (level <= levels.INFO) { + return; + } logger.debug(`BEGIN FLOW: ${flow}`); }, variable: ({ name, value }) => { - if (level <= levels.DEBUG) return; + if (level <= levels.DEBUG) { + return; + } // Check if the variable name matches any of the redact patterns and redact the value let sanitizedValue = value; for (const pattern of redactPatterns) { @@ -118,7 +138,9 @@ module.exports = { logger.debug({ variable: { name, value: sanitizedValue } }, `VARIABLE ${name}`); }, request: () => (req, res, next) => { - if (level < levels.DEBUG) return next(); + if (level < levels.DEBUG) { + return next(); + } logger.debug({ query: req.query, body: req.body }, `Hit URL ${req.url} with following`); return next(); }, diff --git a/api/utils/abortMessage.js b/api/utils/abortMessage.js index 24c56479ebb..fea33eb4c79 100644 --- a/api/utils/abortMessage.js +++ b/api/utils/abortMessage.js @@ -15,4 +15,4 @@ async function abortMessage(req, res, abortControllers) { res.send(JSON.stringify(ret)); } -module.exports = abortMessage; \ No newline at end of file +module.exports = abortMessage; diff --git a/api/utils/azureUtils.js b/api/utils/azureUtils.js index 6330e6080c7..10df919f1aa 100644 --- a/api/utils/azureUtils.js +++ b/api/utils/azureUtils.js @@ -1,6 +1,6 @@ const genAzureEndpoint = ({ azureOpenAIApiInstanceName, azureOpenAIApiDeploymentName }) => { return `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${azureOpenAIApiDeploymentName}`; -} +}; const genAzureChatCompletion = ({ azureOpenAIApiInstanceName, @@ -8,7 +8,7 @@ const genAzureChatCompletion = ({ azureOpenAIApiVersion, }) => { return `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${azureOpenAIApiDeploymentName}/chat/completions?api-version=${azureOpenAIApiVersion}`; -} +}; const getAzureCredentials = () => { return { @@ -16,7 +16,7 @@ const getAzureCredentials = () => { azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME, azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME, azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION, - } -} + }; +}; module.exports = { genAzureEndpoint, genAzureChatCompletion, getAzureCredentials }; diff --git a/api/utils/debug.js b/api/utils/debug.js index 579d2c11295..68599eea387 100644 --- a/api/utils/debug.js +++ b/api/utils/debug.js @@ -12,21 +12,29 @@ module.exports = { setLevel: (l) => (level = l), log: { parameters: (parameters) => { - if (levels.HIGH > level) return; + if (levels.HIGH > level) { + return; + } console.group(); parameters.forEach((p) => console.log(`${p.name}:`, p.value)); console.groupEnd(); }, functionName: (name) => { - if (levels.MEDIUM > level) return; + if (levels.MEDIUM > level) { + return; + } console.log(`\nEXECUTING: ${name}\n`); }, flow: (flow) => { - if (levels.LOW > level) return; + if (levels.LOW > level) { + return; + } console.log(`\n\n\nBEGIN FLOW: ${flow}\n\n\n`); }, variable: ({ name, value }) => { - if (levels.HIGH > level) return; + if (levels.HIGH > level) { + return; + } console.group(); console.group(); console.log(`VARIABLE ${name}:`, value); @@ -34,7 +42,9 @@ module.exports = { console.groupEnd(); }, request: () => (req, res, next) => { - if (levels.HIGH > level) return next(); + if (levels.HIGH > level) { + return next(); + } console.log('Hit URL', req.url, 'with following:'); console.group(); console.log('Query:', req.query); diff --git a/api/utils/findMessageContent.js b/api/utils/findMessageContent.js new file mode 100644 index 00000000000..c5064350310 --- /dev/null +++ b/api/utils/findMessageContent.js @@ -0,0 +1,33 @@ +function findContent(obj) { + if (obj && typeof obj === 'object') { + if ('kwargs' in obj && 'content' in obj.kwargs) { + return obj.kwargs.content; + } + for (let key in obj) { + let content = findContent(obj[key]); + if (content) { + return content; + } + } + } + return null; +} + +function findMessageContent(message) { + let startIndex = Math.min(message.indexOf('{'), message.indexOf('[')); + let jsonString = message.substring(startIndex); + + let jsonObjectOrArray; + try { + jsonObjectOrArray = JSON.parse(jsonString); + } catch (error) { + console.error('Failed to parse JSON:', error); + return null; + } + + let content = findContent(jsonObjectOrArray); + + return content; +} + +module.exports = findMessageContent; diff --git a/api/utils/index.js b/api/utils/index.js index 6a7ff501d75..0a4dd75bf5f 100644 --- a/api/utils/index.js +++ b/api/utils/index.js @@ -3,6 +3,7 @@ const cryptoUtils = require('./crypto'); const { tiktokenModels, maxTokensMap } = require('./tokens'); const sendEmail = require('./sendEmail'); const abortMessage = require('./abortMessage'); +const findMessageContent = require('./findMessageContent'); module.exports = { ...cryptoUtils, @@ -11,4 +12,5 @@ module.exports = { tiktokenModels, sendEmail, abortMessage, -} \ No newline at end of file + findMessageContent, +}; diff --git a/client/src/components/Auth/Login.tsx b/client/src/components/Auth/Login.tsx index 8faf678d589..836452a496b 100644 --- a/client/src/components/Auth/Login.tsx +++ b/client/src/components/Auth/Login.tsx @@ -7,7 +7,7 @@ import { useRecoilValue } from 'recoil'; import store from '~/store'; import { localize } from '~/localization/Translation'; import { useGetStartupConfig } from '@librechat/data-provider'; -import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components' +import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components'; function Login() { const { login, error, isAuthenticated } = useAuthContext(); @@ -26,7 +26,9 @@ function Login() { return (
-

{localize(lang, 'com_auth_welcome_back')}

+

+ {localize(lang, 'com_auth_welcome_back')} +

{error && (
-
+ href={`${startupConfig.serverDomain}/oauth/google`} + >

{localize(lang, 'com_auth_google_login')}

@@ -87,12 +89,12 @@ function Login() { )} {startupConfig?.githubLoginEnabled && startupConfig?.socialLoginEnabled && ( <> -
+ href={`${startupConfig.serverDomain}/oauth/github`} + >

{localize(lang, 'com_auth_github_login')}

@@ -101,12 +103,12 @@ function Login() { )} {startupConfig?.discordLoginEnabled && startupConfig?.socialLoginEnabled && ( <> -
); -}; +} export default Login; diff --git a/client/src/components/Auth/Registration.tsx b/client/src/components/Auth/Registration.tsx index 296f7fc866f..dec13610797 100644 --- a/client/src/components/Auth/Registration.tsx +++ b/client/src/components/Auth/Registration.tsx @@ -9,7 +9,7 @@ import { TRegisterUser, useGetStartupConfig, } from '@librechat/data-provider'; -import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components' +import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components'; function Registration() { const navigate = useNavigate(); @@ -235,7 +235,8 @@ function Registration() { // return false; // }} {...register('confirm_password', { - validate: (value) => value === password || localize(lang, 'com_auth_password_not_match'), + validate: (value) => + value === password || localize(lang, 'com_auth_password_not_match'), })} aria-invalid={!!errors.confirm_password} className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0" @@ -294,12 +295,12 @@ function Registration() { )} {startupConfig?.googleLoginEnabled && startupConfig?.socialLoginEnabled && ( <> -
+ href={`${startupConfig.serverDomain}/oauth/google`} + >

{localize(lang, 'com_auth_google_login')}

@@ -326,13 +327,12 @@ function Registration() { )} {startupConfig?.githubLoginEnabled && startupConfig?.socialLoginEnabled && ( <> -
+ href={`${startupConfig.serverDomain}/oauth/github`} + >

{localize(lang, 'com_auth_github_login')}

@@ -341,12 +341,12 @@ function Registration() { )} {startupConfig?.discordLoginEnabled && startupConfig?.socialLoginEnabled && ( <> -
+ href={`${startupConfig.serverDomain}/oauth/discord`} + >

{localize(lang, 'com_auth_discord_login')}

diff --git a/client/src/components/Auth/RequestPasswordReset.tsx b/client/src/components/Auth/RequestPasswordReset.tsx index fad71e8edbd..8f493d3d5f9 100644 --- a/client/src/components/Auth/RequestPasswordReset.tsx +++ b/client/src/components/Auth/RequestPasswordReset.tsx @@ -39,7 +39,9 @@ function RequestPasswordReset() { return (
-

{localize(lang, 'com_auth_reset_password')}

+

+ {localize(lang, 'com_auth_reset_password')} +

{success && (
-

{localize(lang, 'com_auth_reset_password_success')}

+

+ {localize(lang, 'com_auth_reset_password_success')} +

-

{localize(lang, 'com_auth_reset_password')}

+

+ {localize(lang, 'com_auth_reset_password')} +

{resetError && (
value === password || localize(lang, 'com_auth_password_not_match'), + validate: (value) => + value === password || localize(lang, 'com_auth_password_not_match'), })} aria-invalid={!!errors.confirm_password} className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0" diff --git a/client/src/components/Auth/__tests__/LoginForm.spec.tsx b/client/src/components/Auth/__tests__/LoginForm.spec.tsx index ef998171af0..89a5a66aace 100644 --- a/client/src/components/Auth/__tests__/LoginForm.spec.tsx +++ b/client/src/components/Auth/__tests__/LoginForm.spec.tsx @@ -11,7 +11,7 @@ test('renders login form', () => { }); test('submits login form', async () => { - const { getByLabelText, getByRole } = render(); + const { getByLabelText, getByRole } = render(); const emailInput = getByLabelText(/email/i); const passwordInput = getByLabelText(/password/i); const submitButton = getByRole('button', { name: /Sign in/i }); @@ -24,7 +24,7 @@ test('submits login form', async () => { }); test('displays validation error messages', async () => { - const { getByLabelText, getByRole, getByText } = render(); + const { getByLabelText, getByRole, getByText } = render(); const emailInput = getByLabelText(/email/i); const passwordInput = getByLabelText(/password/i); const submitButton = getByRole('button', { name: /Sign in/i }); @@ -36,4 +36,3 @@ test('displays validation error messages', async () => { expect(getByText(/You must enter a valid email address/i)).toBeInTheDocument(); expect(getByText(/Password must be at least 8 characters/i)).toBeInTheDocument(); }); - diff --git a/client/src/components/Conversations/DeleteButton.jsx b/client/src/components/Conversations/DeleteButton.jsx index 217aab0b348..d2e0b8166dd 100644 --- a/client/src/components/Conversations/DeleteButton.jsx +++ b/client/src/components/Conversations/DeleteButton.jsx @@ -15,7 +15,9 @@ export default function DeleteButton({ conversationId, renaming, cancelHandler, useEffect(() => { if (deleteConvoMutation.isSuccess) { - if (currentConversation?.conversationId == conversationId) newConversation(); + if (currentConversation?.conversationId == conversationId) { + newConversation(); + } refreshConversations(); retainView(); diff --git a/client/src/components/Endpoints/Anthropic/OptionHover.jsx b/client/src/components/Endpoints/Anthropic/OptionHover.jsx index 0e8a3c5fd85..b7c7f812474 100644 --- a/client/src/components/Endpoints/Anthropic/OptionHover.jsx +++ b/client/src/components/Endpoints/Anthropic/OptionHover.jsx @@ -12,10 +12,7 @@ const types = { function OptionHover({ type, side }) { return ( - +

{types[type]}

diff --git a/client/src/components/Endpoints/EditPresetDialog.jsx b/client/src/components/Endpoints/EditPresetDialog.jsx index 338d5dd73c5..5ac77de5f9c 100644 --- a/client/src/components/Endpoints/EditPresetDialog.jsx +++ b/client/src/components/Endpoints/EditPresetDialog.jsx @@ -168,7 +168,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
@@ -227,7 +227,9 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
{shouldShowSettings && } - {preset?.endpoint === 'google' && showExamples && !preset?.model?.startsWith('codechat-') && ( + {preset?.endpoint === 'google' && + showExamples && + !preset?.model?.startsWith('codechat-') && (
@@ -42,7 +42,10 @@ function EndpointOptionsPopover({ {additionalButton && (
); - } else if (!isTokenProvided && (!endpointsToHideSetTokens.has(endpoint))) { + } else if (!isTokenProvided && !endpointsToHideSetTokens.has(endpoint)) { return ( <> event.preventDefault()} > - {localize(lang, 'com_ui_select_model')} + + {localize(lang, 'com_ui_select_model')} + {availableModels.map((model) => ( diff --git a/client/src/components/ui/Switch.tsx b/client/src/components/ui/Switch.tsx index a3be6d4aed0..304b07f61a6 100644 --- a/client/src/components/ui/Switch.tsx +++ b/client/src/components/ui/Switch.tsx @@ -1,5 +1,5 @@ -import * as React from 'react' -import * as SwitchPrimitives from '@radix-ui/react-switch' +import * as React from 'react'; +import * as SwitchPrimitives from '@radix-ui/react-switch'; import { cn } from '../../utils'; @@ -9,7 +9,7 @@ const Switch = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -Switch.displayName = SwitchPrimitives.Root.displayName +)); +Switch.displayName = SwitchPrimitives.Root.displayName; -export { Switch } \ No newline at end of file +export { Switch }; diff --git a/client/src/components/ui/Templates.jsx b/client/src/components/ui/Templates.jsx index fc5bec25786..55dab7514f0 100644 --- a/client/src/components/ui/Templates.jsx +++ b/client/src/components/ui/Templates.jsx @@ -16,9 +16,13 @@ export default function Templates({ showTemplates }) {
- {localize(lang, 'com_ui_showing')} 1 {localize(lang, 'com_ui_of')}{' '} + {localize(lang, 'com_ui_showing')}{' '} + 1{' '} + {localize(lang, 'com_ui_of')}{' '} - 1 {localize(lang, 'com_ui_entries')} + + 1 {localize(lang, 'com_ui_entries')} +