diff --git a/.eslintrc.js b/.eslintrc.js index d1c54c150..f0c7505ee 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 cb3431ea5..c0f746d73 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 1db564ceb..97674574c 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 43054aa6e..2e84304c3 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 000000000..e3caf6e2c --- /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 000000000..8691f0ccf --- /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 000000000..0eb31e37c --- /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 000000000..8b92e6e38 --- /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 000000000..d68c919eb --- /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 000000000..cb3affc8b --- /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 000000000..34cca8296 --- /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 000000000..53cf06154 --- /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 000000000..1900a926c --- /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 000000000..6d00d490d --- /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 000000000..5fe7f1cb3 --- /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 70fcd76c3..a2968135c 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 000000000..2d5756f19 --- /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 000000000..21ff4eb8c --- /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 018eb0bcd..13bf2fe18 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 000000000..d98e6c645 --- /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 000000000..7b906d86f --- /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 d25a30221..a3469996a 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 8f9cbe9dd..8fc1cea8b 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 f8c4d4a8a..f99363d14 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 c7bf9e39e..1abcff9da 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 37235bebe..837d3dee6 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 8421e3e90..e6ea9ce75 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 b09055d01..a42d2c177 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 68b101fd8..3325d84fc 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 13c2fd0d4..92e064480 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 296f90333..4b4251dda 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 86eb78bd2..7371c9721 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 442af996e..34631e744 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 1d32f306a..cdfd5b97a 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 1f6d35064..304c08965 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 dddcadf47..0c7cf271f 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 c4561c0a4..29bc70b7b 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 aa9bcef98..c574432a1 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 c3f0ba778..87ce05af0 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 c50aa97b3..58f4aba8e 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 9e4e8aace..576f58108 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 f80a08c37..e03529527 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 5d5a16376..f3d25cbcd 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 c54129f47..c4f8a3fc2 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 c99432a16..d917c65ca 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 03795423d..608aca2e3 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 3bb04b414..cf1611db3 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 5eb56a2df..29a88de99 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 1b54b77fa..a556939dd 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 59a1d6051..5ae708d85 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 71bef1a40..2467479fc 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 6d2511b4c..cf49fa24a 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 imagePath = path.join( + __dirname, + '..', + '..', + 'client', + 'public', + 'images', + 'openid', + fileName, + ); - const imagePathOrEmpty = await downloadImage(imageUrl, imagePath, tokenset.access_token); + 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 fdb728513..d0e78821f 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 24c56479e..fea33eb4c 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 6330e6080..10df919f1 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 579d2c112..68599eea3 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 000000000..c50643503 --- /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 6a7ff501d..0a4dd75bf 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 8faf678d5..836452a49 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 296f7fc86..dec136107 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 fad71e8ed..8f493d3d5 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 ef998171a..89a5a66aa 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 217aab0b3..d2e0b8166 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 0e8a3c5fd..b7c7f8124 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 338d5dd73..5ac77de5f 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 a3be6d4ae..304b07f61 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 fc5bec257..55dab7514 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')} +