diff --git a/.env.example b/.env.example index 2c97892d61..131a9b326f 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,8 @@ APP_TITLE=LibreChat # CUSTOM_FOOTER="My custom footer" +DEBUG_LOGGING=true + HOST=localhost PORT=3080 diff --git a/api/app/bingai.js b/api/app/bingai.js index 56a488541c..f7ecf4462d 100644 --- a/api/app/bingai.js +++ b/api/app/bingai.js @@ -1,6 +1,7 @@ require('dotenv').config(); const { KeyvFile } = require('keyv-file'); -const { getUserKey, checkUserKeyExpiry } = require('../server/services/UserService'); +const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService'); +const { logger } = require('~/config'); const askBing = async ({ text, @@ -100,7 +101,7 @@ const askBing = async ({ } } - console.log('bing options', options); + logger.debug('bing options', options); const res = await bingAIClient.sendMessage(text, options); diff --git a/api/app/clients/AnthropicClient.js b/api/app/clients/AnthropicClient.js index 5174a1d197..25c642412a 100644 --- a/api/app/clients/AnthropicClient.js +++ b/api/app/clients/AnthropicClient.js @@ -3,6 +3,7 @@ const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = requ const { getResponseSender, EModelEndpoint } = require('librechat-data-provider'); const { getModelMaxTokens } = require('~/utils'); const BaseClient = require('./BaseClient'); +const { logger } = require('~/config'); const HUMAN_PROMPT = '\n\nHuman:'; const AI_PROMPT = '\n\nAssistant:'; @@ -103,9 +104,8 @@ class AnthropicClient extends BaseClient { messages, parentMessageId, }); - if (this.options.debug) { - console.debug('AnthropicClient: orderedMessages', orderedMessages, parentMessageId); - } + + logger.debug('[AnthropicClient] orderedMessages', { orderedMessages, parentMessageId }); const formattedMessages = orderedMessages.map((message) => ({ author: message.isCreatedByUser ? this.userLabel : this.assistantLabel, @@ -247,7 +247,7 @@ class AnthropicClient extends BaseClient { } getCompletion() { - console.log('AnthropicClient doesn\'t use getCompletion (all handled in sendCompletion)'); + logger.debug('AnthropicClient doesn\'t use getCompletion (all handled in sendCompletion)'); } async sendCompletion(payload, { onProgress, abortController }) { @@ -262,12 +262,7 @@ class AnthropicClient extends BaseClient { modelOptions.stream = true; } - const { debug } = this.options; - if (debug) { - console.debug(); - console.debug(modelOptions); - console.debug(); - } + logger.debug('modelOptions', { modelOptions }); const client = this.getClient(); const metadata = { @@ -295,32 +290,23 @@ class AnthropicClient extends BaseClient { top_p, top_k, }; - if (this.options.debug) { - console.log('AnthropicClient: requestOptions'); - console.dir(requestOptions, { depth: null }); - } + logger.debug('[AnthropicClient]', { requestOptions }); const response = await client.completions.create(requestOptions); signal.addEventListener('abort', () => { - if (this.options.debug) { - console.log('AnthropicClient: message aborted!'); - } + logger.debug('[AnthropicClient] message aborted!'); response.controller.abort(); }); for await (const completion of response) { - if (this.options.debug) { - // Uncomment to debug message stream - // console.debug(completion); - } + // Uncomment to debug message stream + // logger.debug(completion); text += completion.completion; onProgress(completion.completion); } signal.removeEventListener('abort', () => { - if (this.options.debug) { - console.log('AnthropicClient: message aborted!'); - } + logger.debug('[AnthropicClient] message aborted!'); response.controller.abort(); }); @@ -336,9 +322,7 @@ class AnthropicClient extends BaseClient { } getBuildMessagesOptions() { - if (this.options.debug) { - console.log('AnthropicClient doesn\'t use getBuildMessagesOptions'); - } + logger.debug('AnthropicClient doesn\'t use getBuildMessagesOptions'); } static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) { diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index 626a98888d..3f05a297fc 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -3,6 +3,7 @@ const TextStream = require('./TextStream'); const { getConvo, getMessages, saveMessage, updateMessage, saveConvo } = require('~/models'); const { addSpaceIfNeeded, isEnabled } = require('~/server/utils'); const checkBalance = require('~/models/checkBalance'); +const { logger } = require('~/config'); class BaseClient { constructor(apiKey, options = {}) { @@ -41,15 +42,14 @@ class BaseClient { } async getTokenCountForResponse(response) { - if (this.options.debug) { - console.debug('`recordTokenUsage` not implemented.', response); - } + logger.debug('`[BaseClient] recordTokenUsage` not implemented.', response); } async recordTokenUsage({ promptTokens, completionTokens }) { - if (this.options.debug) { - console.debug('`recordTokenUsage` not implemented.', { promptTokens, completionTokens }); - } + logger.debug('`[BaseClient] recordTokenUsage` not implemented.', { + promptTokens, + completionTokens, + }); } getBuildMessagesOptions() { @@ -194,14 +194,14 @@ class BaseClient { const update = {}; if (messageId === tokenCountMap.summaryMessage?.messageId) { - this.options.debug && console.debug(`Adding summary props to ${messageId}.`); + logger.debug(`[BaseClient] Adding summary props to ${messageId}.`); update.summary = tokenCountMap.summaryMessage.content; update.summaryTokenCount = tokenCountMap.summaryMessage.tokenCount; } if (message.tokenCount && !update.summaryTokenCount) { - this.options.debug && console.debug(`Skipping ${messageId}: already had a token count.`); + logger.debug(`[BaseClient] Skipping ${messageId}: already had a token count.`); continue; } @@ -278,19 +278,17 @@ class BaseClient { if (instructions) { ({ tokenCount, ..._instructions } = instructions); } - this.options.debug && _instructions && console.debug('instructions tokenCount', tokenCount); + _instructions && logger.debug('[BaseClient] instructions tokenCount: ' + tokenCount); let payload = this.addInstructions(formattedMessages, _instructions); let orderedWithInstructions = this.addInstructions(orderedMessages, instructions); let { context, remainingContextTokens, messagesToRefine, summaryIndex } = await this.getMessagesWithinTokenLimit(orderedWithInstructions); - this.options.debug && - console.debug( - 'remainingContextTokens, this.maxContextTokens (1/2)', - remainingContextTokens, - this.maxContextTokens, - ); + logger.debug('[BaseClient] Context Count (1/2)', { + remainingContextTokens, + maxContextTokens: this.maxContextTokens, + }); let summaryMessage; let summaryTokenCount; @@ -308,10 +306,9 @@ class BaseClient { if (diff > 0) { payload = payload.slice(diff); - this.options.debug && - console.debug( - `Difference between original payload (${length}) and context (${context.length}): ${diff}`, - ); + logger.debug( + `[BaseClient] Difference between original payload (${length}) and context (${context.length}): ${diff}`, + ); } const latestMessage = orderedWithInstructions[orderedWithInstructions.length - 1]; @@ -338,12 +335,10 @@ class BaseClient { // Make sure to only continue summarization logic if the summary message was generated shouldSummarize = summaryMessage && shouldSummarize; - this.options.debug && - console.debug( - 'remainingContextTokens, this.maxContextTokens (2/2)', - remainingContextTokens, - this.maxContextTokens, - ); + logger.debug('[BaseClient] Context Count (2/2)', { + remainingContextTokens, + maxContextTokens: this.maxContextTokens, + }); let tokenCountMap = orderedWithInstructions.reduce((map, message, index) => { const { messageId } = message; @@ -361,19 +356,13 @@ class BaseClient { const promptTokens = this.maxContextTokens - remainingContextTokens; - if (this.options.debug) { - console.debug('<-------------------------PAYLOAD/TOKEN COUNT MAP------------------------->'); - console.debug('Payload:', payload); - console.debug('Token Count Map:', tokenCountMap); - console.debug( - 'Prompt Tokens', - promptTokens, - 'remainingContextTokens', - remainingContextTokens, - 'this.maxContextTokens', - this.maxContextTokens, - ); - } + logger.debug('[BaseClient] Payload size:', payload.length); + logger.debug('[BaseClient] tokenCountMap:', tokenCountMap); + logger.debug('[BaseClient]', { + promptTokens, + remainingContextTokens, + maxContextTokens: this.maxContextTokens, + }); return { payload, tokenCountMap, promptTokens, messages: orderedWithInstructions }; } @@ -421,11 +410,11 @@ class BaseClient { ); if (tokenCountMap) { - console.dir(tokenCountMap, { depth: null }); + logger.debug('[BaseClient] tokenCountMap', tokenCountMap); if (tokenCountMap[userMessage.messageId]) { userMessage.tokenCount = tokenCountMap[userMessage.messageId]; - console.log('userMessage.tokenCount', userMessage.tokenCount); - console.log('userMessage', userMessage); + logger.debug('[BaseClient] userMessage.tokenCount', userMessage.tokenCount); + logger.debug('[BaseClient] userMessage', userMessage); } this.handleTokenCountMap(tokenCountMap); @@ -443,7 +432,6 @@ class BaseClient { user: this.user, tokenType: 'prompt', amount: promptTokens, - debug: this.options.debug, model: this.modelOptions.model, endpoint: this.options.endpoint, }, @@ -483,9 +471,7 @@ class BaseClient { } async loadHistory(conversationId, parentMessageId = null) { - if (this.options.debug) { - console.debug('Loading history for conversation', conversationId, parentMessageId); - } + logger.debug('[BaseClient] Loading history:', { conversationId, parentMessageId }); const messages = (await getMessages({ conversationId })) ?? []; @@ -516,9 +502,14 @@ class BaseClient { } } - if (this.options.debug && this.previous_summary) { + if (this.previous_summary) { const { messageId, summary, tokenCount, summaryTokenCount } = this.previous_summary; - console.debug('Previous summary:', { messageId, summary, tokenCount, summaryTokenCount }); + logger.debug('[BaseClient] Previous summary:', { + messageId, + summary, + tokenCount, + summaryTokenCount, + }); } return orderedMessages; diff --git a/api/app/clients/GoogleClient.js b/api/app/clients/GoogleClient.js index 4d5ee00211..5454942079 100644 --- a/api/app/clients/GoogleClient.js +++ b/api/app/clients/GoogleClient.js @@ -8,6 +8,7 @@ const { getResponseSender, EModelEndpoint, endpointSettings } = require('librech const { getModelMaxTokens } = require('~/utils'); const { formatMessage } = require('./prompts'); const BaseClient = require('./BaseClient'); +const { logger } = require('~/config'); const loc = 'us-central1'; const publisher = 'google'; @@ -42,8 +43,7 @@ class GoogleClient extends BaseClient { jwtClient.authorize((err) => { if (err) { - console.error('Error: jwtClient failed to authorize'); - console.error(err.message); + logger.error('jwtClient failed to authorize', err); throw err; } }); @@ -58,11 +58,9 @@ class GoogleClient extends BaseClient { return new Promise((resolve, reject) => { jwtClient.authorize((err, tokens) => { if (err) { - console.error('Error: jwtClient failed to authorize'); - console.error(err.message); + logger.error('jwtClient failed to authorize', err); reject(err); } else { - console.log('Access Token:', tokens.access_token); resolve(tokens.access_token); } }); @@ -213,8 +211,7 @@ class GoogleClient extends BaseClient { } if (this.options.debug) { - console.debug('GoogleClient buildMessages'); - console.dir(payload, { depth: null }); + logger.debug('GoogleClient buildMessages', payload); } return { prompt: payload }; @@ -226,7 +223,10 @@ class GoogleClient extends BaseClient { parentMessageId, }); if (this.options.debug) { - console.debug('GoogleClient: orderedMessages', orderedMessages, parentMessageId); + logger.debug('GoogleClient: orderedMessages, parentMessageId', { + orderedMessages, + parentMessageId, + }); } const formattedMessages = orderedMessages.map((message) => ({ @@ -377,10 +377,7 @@ class GoogleClient extends BaseClient { const { debug } = this.options; const url = this.completionsUrl; if (debug) { - console.debug(); - console.debug(url); - console.debug(this.modelOptions); - console.debug(); + logger.debug('GoogleClient _getCompletion', { url, payload }); } const opts = { method: 'POST', @@ -397,7 +394,7 @@ class GoogleClient extends BaseClient { const client = await this.getClient(); const res = await client.request({ url, method: 'POST', data: payload }); - console.dir(res.data, { depth: null }); + logger.debug('GoogleClient _getCompletion', { res }); return res.data; } @@ -476,7 +473,7 @@ class GoogleClient extends BaseClient { } getBuildMessagesOptions() { - // console.log('GoogleClient doesn\'t use getBuildMessagesOptions'); + // logger.debug('GoogleClient doesn\'t use getBuildMessagesOptions'); } async sendCompletion(payload, opts = {}) { @@ -484,13 +481,10 @@ class GoogleClient extends BaseClient { try { reply = await this.getCompletion(payload, opts); if (this.options.debug) { - console.debug('result'); - console.debug(reply); + logger.debug('GoogleClient sendCompletion', { reply }); } } catch (err) { - console.error('Error: failed to send completion to Google'); - console.error(err); - console.error(err.message); + logger.error('failed to send completion to Google', err); } return reply.trim(); } diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index 928c2416f5..cd84194bfd 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -14,6 +14,7 @@ const { summaryBuffer } = require('./memory'); const { runTitleChain } = require('./chains'); const { tokenSplit } = require('./document'); const BaseClient = require('./BaseClient'); +const { logger } = require('~/config'); // Cache to store Tiktoken instances const tokenizersCache = {}; @@ -123,7 +124,7 @@ class OpenAIClient extends BaseClient { } if (this.options.debug) { - console.debug('maxContextTokens', this.maxContextTokens); + logger.debug('[OpenAIClient] maxContextTokens', this.maxContextTokens); } this.maxResponseTokens = this.modelOptions.max_tokens || 1024; @@ -175,7 +176,7 @@ class OpenAIClient extends BaseClient { } if (this.azureEndpoint && this.options.debug) { - console.debug('Using Azure endpoint'); + logger.debug('Using Azure endpoint'); } if (this.useOpenRouter) { @@ -254,8 +255,7 @@ class OpenAIClient extends BaseClient { // Reset count tokenizerCallsCount = 1; } catch (error) { - console.log('Free and reset encoders error'); - console.error(error); + logger.error('[OpenAIClient] Free and reset encoders error', error); } } @@ -263,7 +263,7 @@ class OpenAIClient extends BaseClient { resetTokenizersIfNecessary() { if (tokenizerCallsCount >= 25) { if (this.options.debug) { - console.debug('freeAndResetAllEncoders: reached 25 encodings, resetting...'); + logger.debug('[OpenAIClient] freeAndResetAllEncoders: reached 25 encodings, resetting...'); } this.constructor.freeAndResetAllEncoders(); } @@ -403,11 +403,6 @@ class OpenAIClient extends BaseClient { return; } - if (this.options.debug) { - // console.debug('progressMessage'); - // console.dir(progressMessage, { depth: null }); - } - if (progressMessage.choices) { streamResult = progressMessage; } @@ -427,9 +422,7 @@ class OpenAIClient extends BaseClient { if (!token) { return; } - if (this.options.debug) { - // console.debug(token); - } + if (token === this.endToken) { return; } @@ -451,9 +444,9 @@ class OpenAIClient extends BaseClient { null, opts.abortController || new AbortController(), ); - if (this.options.debug) { - console.debug(JSON.stringify(result)); - } + + logger.debug('[OpenAIClient] sendCompletion: result', result); + if (this.isChatCompletion) { reply = result.choices[0].message.content; } else { @@ -557,11 +550,13 @@ class OpenAIClient extends BaseClient { title = await runTitleChain({ llm, text, convo, signal: this.abortController.signal }); } catch (e) { if (e?.message?.toLowerCase()?.includes('abort')) { - this.options.debug && console.debug('Aborted title generation'); + logger.debug('[OpenAIClient] Aborted title generation'); return; } - console.log('There was an issue generating title with LangChain, trying the old method...'); - this.options.debug && console.error(e.message, e); + logger.error( + '[OpenAIClient] There was an issue generating title with LangChain, trying the old method...', + e, + ); modelOptions.model = OPENAI_TITLE_MODEL ?? 'gpt-3.5-turbo'; if (this.azure) { modelOptions.model = process.env.AZURE_OPENAI_DEFAULT_MODEL ?? modelOptions.model; @@ -582,17 +577,16 @@ ${convo} try { title = (await this.sendPayload(instructionsPayload, { modelOptions })).replaceAll('"', ''); } catch (e) { - console.error(e); - console.log('There was another issue generating the title, see error above.'); + logger.error('[OpenAIClient] There was another issue generating the title', e); } } - console.log('CONVERSATION TITLE', title); + logger.debug('[OpenAIClient] Convo Title: ' + title); return title; } async summarizeMessages({ messagesToRefine, remainingContextTokens }) { - this.options.debug && console.debug('Summarizing messages...'); + logger.debug('[OpenAIClient] Summarizing messages...'); let context = messagesToRefine; let prompt; @@ -615,8 +609,9 @@ ${convo} } if (context.length === 0) { - this.options.debug && - console.debug('Summary context is empty, using latest message within token limit'); + logger.debug( + '[OpenAIClient] Summary context is empty, using latest message within token limit', + ); promptBuffer = 32; const { text, ...latestMessage } = messagesToRefine[messagesToRefine.length - 1]; @@ -643,7 +638,7 @@ ${convo} // by recreating the summary prompt (single message) to avoid LangChain handling const initialPromptTokens = this.maxContextTokens - remainingContextTokens; - this.options.debug && console.debug(`initialPromptTokens: ${initialPromptTokens}`); + logger.debug('[OpenAIClient] initialPromptTokens', initialPromptTokens); const llm = this.initializeLLM({ model: OPENAI_SUMMARY_MODEL, @@ -669,9 +664,9 @@ ${convo} const summaryTokenCount = this.getTokenCountForMessage(summaryMessage); if (this.options.debug) { - console.debug('summaryMessage:', summaryMessage); - console.debug( - `remainingContextTokens: ${remainingContextTokens}, after refining: ${ + logger.debug('[OpenAIClient] summaryTokenCount', summaryTokenCount); + logger.debug( + `[OpenAIClient] Summarization complete: remainingContextTokens: ${remainingContextTokens}, after refining: ${ remainingContextTokens - summaryTokenCount }`, ); @@ -680,7 +675,7 @@ ${convo} return { summaryMessage, summaryTokenCount }; } catch (e) { if (e?.message?.toLowerCase()?.includes('abort')) { - this.options.debug && console.debug('Aborted summarization'); + logger.debug('[OpenAIClient] Aborted summarization'); const { run, runId } = this.runManager.getRunByConversationId(this.conversationId); if (run && run.error) { const { error } = run; @@ -688,17 +683,13 @@ ${convo} throw new Error(error); } } - console.error('Error summarizing messages'); - this.options.debug && console.error(e); + logger.error('[OpenAIClient] Error summarizing messages', e); return {}; } } async recordTokenUsage({ promptTokens, completionTokens }) { - if (this.options.debug) { - console.debug('promptTokens', promptTokens); - console.debug('completionTokens', completionTokens); - } + logger.debug('[OpenAIClient]', { promptTokens, completionTokens }); await spendTokens( { user: this.user, @@ -736,14 +727,19 @@ ${convo} modelOptions.prompt = payload; } - const { debug } = this.options; - const url = extractBaseURL(this.completionsUrl); - if (debug) { - console.debug('baseURL', url); - console.debug('modelOptions', modelOptions); - } + const baseURL = extractBaseURL(this.completionsUrl); + // let { messages: _msgsToLog, ...modelOptionsToLog } = modelOptions; + // if (modelOptionsToLog.messages) { + // _msgsToLog = modelOptionsToLog.messages.map((msg) => { + // let { content, ...rest } = msg; + + // if (content) + // return { ...rest, content: truncateText(content) }; + // }); + // } + logger.debug('[OpenAIClient] chatCompletion', { baseURL, modelOptions }); const opts = { - baseURL: url, + baseURL, }; if (this.useOpenRouter) { @@ -820,7 +816,7 @@ ${convo} if (!chatCompletion && UnexpectedRoleError) { throw new Error( - 'OpenAIError: Invalid final message: OpenAI expects final message to include role=assistant', + 'OpenAI error: Invalid final message: OpenAI expects final message to include role=assistant', ); } else if (!chatCompletion && error) { throw new Error(error); @@ -843,27 +839,23 @@ ${convo} } if ( err?.message?.includes( - 'OpenAIError: Invalid final message: OpenAI expects final message to include role=assistant', + 'OpenAI error: Invalid final message: OpenAI expects final message to include role=assistant', ) || err?.message?.includes('The server had an error processing your request') || err?.message?.includes('missing finish_reason') || (err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason')) ) { - console.error(err); + logger.error('[OpenAIClient] Known OpenAI error:', err); await abortController.abortCompletion(); return intermediateReply; } else if (err instanceof OpenAI.APIError) { - console.log(err.name); - console.log(err.status); - console.log(err.headers); if (intermediateReply) { return intermediateReply; } else { throw err; } } else { - console.warn('[OpenAIClient.chatCompletion] Unhandled error type'); - console.error(err); + logger.error('[OpenAIClient.chatCompletion] Unhandled error type', err); throw err; } } diff --git a/api/app/clients/PluginsClient.js b/api/app/clients/PluginsClient.js index 721afa7c95..509b98ca6f 100644 --- a/api/app/clients/PluginsClient.js +++ b/api/app/clients/PluginsClient.js @@ -10,6 +10,7 @@ const { SelfReflectionTool } = require('./tools'); const { isEnabled } = require('~/server/utils'); const { extractBaseURL } = require('~/utils'); const { loadTools } = require('./tools/util'); +const { logger } = require('~/config'); class PluginsClient extends OpenAIClient { constructor(apiKey, options = {}) { @@ -85,17 +86,15 @@ class PluginsClient extends OpenAIClient { initialMessageCount: this.currentMessages.length + 1, }); - if (this.options.debug) { - console.debug( - `<-----Agent Model: ${model.modelName} | Temp: ${model.temperature} | Functions: ${this.functionsAgent}----->`, - ); - } + logger.debug( + `[PluginsClient] Agent Model: ${model.modelName} | Temp: ${model.temperature} | Functions: ${this.functionsAgent}`, + ); // Map Messages to Langchain format const pastMessages = formatLangChainMessages(this.currentMessages.slice(0, -1), { userName: this.options?.name, }); - this.options.debug && console.debug('pastMessages: ', pastMessages); + logger.debug('[PluginsClient] pastMessages: ' + pastMessages.length); // TODO: use readOnly memory, TokenBufferMemory? (both unavailable in LangChainJS) const memory = new BufferMemory({ @@ -124,19 +123,16 @@ class PluginsClient extends OpenAIClient { return; } - if (this.options.debug) { - console.debug('Requested Tools'); - console.debug(this.options.tools); - console.debug('Loaded Tools'); - console.debug(this.tools.map((tool) => tool.name)); - } + logger.debug('[PluginsClient] Requested Tools', this.options.tools); + logger.debug( + '[PluginsClient] Loaded Tools', + this.tools.map((tool) => tool.name), + ); const handleAction = (action, runId, callback = null) => { this.saveLatestAction(action); - if (this.options.debug) { - console.debug('Latest Agent Action ', this.actions[this.actions.length - 1]); - } + logger.debug('[PluginsClient] Latest Agent Action ', this.actions[this.actions.length - 1]); if (typeof callback === 'function') { callback(action, runId); @@ -165,9 +161,7 @@ class PluginsClient extends OpenAIClient { }), }); - if (this.options.debug) { - console.debug('Loaded agent.'); - } + logger.debug('[PluginsClient] Loaded agent.'); } async executorCall(message, { signal, stream, onToolStart, onToolEnd }) { @@ -183,12 +177,10 @@ class PluginsClient extends OpenAIClient { }); const input = attempts > 1 ? errorInput : message; - if (this.options.debug) { - console.debug(`Attempt ${attempts} of ${maxAttempts}`); - } + logger.debug(`[PluginsClient] Attempt ${attempts} of ${maxAttempts}`); - if (this.options.debug && errorMessage.length > 0) { - console.debug('Caught error, input:', input); + if (errorMessage.length > 0) { + logger.debug('[PluginsClient] Caught error, input:', input); } try { @@ -211,10 +203,10 @@ class PluginsClient extends OpenAIClient { ]); break; // Exit the loop if the function call is successful } catch (err) { - console.error(err); + logger.error('[PluginsClient] executorCall error:', err); if (attempts === maxAttempts) { const { run } = this.runManager.getRunByConversationId(this.conversationId); - const defaultOutput = `Encountered an error while attempting to respond. Error: ${err.message}`; + const defaultOutput = `Encountered an error while attempting to respond: ${err.message}`; this.result.output = run && run.error ? run.error : defaultOutput; this.result.errorMessage = run && run.error ? run.error : err.message; this.result.intermediateSteps = this.actions; @@ -226,8 +218,11 @@ class PluginsClient extends OpenAIClient { async handleResponseMessage(responseMessage, saveOptions, user) { const { output, errorMessage, ...result } = this.result; - this.options.debug && - console.debug('[handleResponseMessage] Output:', { output, errorMessage, ...result }); + logger.debug('[PluginsClient][handleResponseMessage] Output:', { + output, + errorMessage, + ...result, + }); const { error } = responseMessage; if (!error) { responseMessage.tokenCount = this.getTokenCountForResponse(responseMessage); @@ -251,7 +246,7 @@ class PluginsClient extends OpenAIClient { this.setOptions(opts); return super.sendMessage(message, opts); } - this.options.debug && console.log('Plugins sendMessage', message, opts); + logger.debug('[PluginsClient] sendMessage', { message, opts }); const { user, isEdited, @@ -281,10 +276,10 @@ class PluginsClient extends OpenAIClient { ); if (tokenCountMap) { - console.dir(tokenCountMap, { depth: null }); + logger.debug('[PluginsClient] tokenCountMap', { tokenCountMap }); if (tokenCountMap[userMessage.messageId]) { userMessage.tokenCount = tokenCountMap[userMessage.messageId]; - console.log('userMessage.tokenCount', userMessage.tokenCount); + logger.debug('[PluginsClient] userMessage.tokenCount', userMessage.tokenCount); } this.handleTokenCountMap(tokenCountMap); } @@ -370,10 +365,7 @@ class PluginsClient extends OpenAIClient { return await this.handleResponseMessage(responseMessage, saveOptions, user); } - if (this.options.debug) { - console.debug('Plugins completion phase: this.result'); - console.debug(this.result); - } + logger.debug('[PluginsClient] Completion phase: this.result', this.result); const promptPrefix = buildPromptPrefix({ result: this.result, @@ -381,28 +373,20 @@ class PluginsClient extends OpenAIClient { functionsAgent: this.functionsAgent, }); - if (this.options.debug) { - console.debug('Plugins: promptPrefix'); - console.debug(promptPrefix); - } + logger.debug('[PluginsClient]', { promptPrefix }); payload = await this.buildCompletionPrompt({ messages: this.currentMessages, promptPrefix, }); - if (this.options.debug) { - console.debug('buildCompletionPrompt Payload'); - console.debug(payload); - } + logger.debug('[PluginsClient] buildCompletionPrompt Payload', payload); responseMessage.text = await this.sendCompletion(payload, opts); return await this.handleResponseMessage(responseMessage, saveOptions, user); } async buildCompletionPrompt({ messages, promptPrefix: _promptPrefix }) { - if (this.options.debug) { - console.debug('buildCompletionPrompt messages', messages); - } + logger.debug('[PluginsClient] buildCompletionPrompt messages', messages); const orderedMessages = messages; let promptPrefix = _promptPrefix.trim(); diff --git a/api/app/clients/TextStream.js b/api/app/clients/TextStream.js index 59ecd82d1a..01809e87fa 100644 --- a/api/app/clients/TextStream.js +++ b/api/app/clients/TextStream.js @@ -1,4 +1,5 @@ const { Readable } = require('stream'); +const { logger } = require('~/config'); class TextStream extends Readable { constructor(text, options = {}) { @@ -38,7 +39,7 @@ class TextStream extends Readable { }); this.on('end', () => { - // console.log('Stream ended'); + // logger.debug('[processTextStream] Stream ended'); resolve(); }); @@ -50,7 +51,7 @@ class TextStream extends Readable { try { await streamPromise; } catch (err) { - console.error('Error processing text stream:', err); + logger.error('[processTextStream] Error in text stream:', err); // Handle the error appropriately, e.g., return an error message or throw an error } } diff --git a/api/app/clients/agents/CustomAgent/outputParser.js b/api/app/clients/agents/CustomAgent/outputParser.js index 80b2d72913..9d849519f5 100644 --- a/api/app/clients/agents/CustomAgent/outputParser.js +++ b/api/app/clients/agents/CustomAgent/outputParser.js @@ -1,4 +1,5 @@ const { ZeroShotAgentOutputParser } = require('langchain/agents'); +const { logger } = require('~/config'); class CustomOutputParser extends ZeroShotAgentOutputParser { constructor(fields) { @@ -64,9 +65,9 @@ class CustomOutputParser extends ZeroShotAgentOutputParser { const match = this.actionValues.exec(text); // old v2 if (!match) { - console.log( - '\n\n<----------------------HIT NO MATCH PARSING ERROR---------------------->\n\n', - match, + logger.debug( + '\n\n<----------------------[CustomOutputParser] HIT NO MATCH PARSING ERROR---------------------->\n\n' + + match, ); const thoughts = text.replace(/[tT]hought:/, '').split('\n'); // return { @@ -84,9 +85,9 @@ class CustomOutputParser extends ZeroShotAgentOutputParser { let selectedTool = match?.[1].trim().toLowerCase(); if (match && selectedTool === 'n/a') { - console.log( - '\n\n<----------------------HIT N/A PARSING ERROR---------------------->\n\n', - match, + logger.debug( + '\n\n<----------------------[CustomOutputParser] HIT N/A PARSING ERROR---------------------->\n\n' + + match, ); return { tool: 'self-reflection', @@ -97,25 +98,25 @@ class CustomOutputParser extends ZeroShotAgentOutputParser { let toolIsValid = this.checkIfValidTool(selectedTool); if (match && !toolIsValid) { - console.log( - '\n\n<----------------Tool invalid: Re-assigning Selected Tool---------------->\n\n', - match, + logger.debug( + '\n\n<----------------[CustomOutputParser] Tool invalid: Re-assigning Selected Tool---------------->\n\n' + + match, ); selectedTool = this.getValidTool(selectedTool); } if (match && !selectedTool) { - console.log( - '\n\n<----------------------HIT INVALID TOOL PARSING ERROR---------------------->\n\n', - match, + logger.debug( + '\n\n<----------------------[CustomOutputParser] HIT INVALID TOOL PARSING ERROR---------------------->\n\n' + + match, ); selectedTool = 'self-reflection'; } if (match && !match[2]) { - console.log( - '\n\n<----------------------HIT NO ACTION INPUT PARSING ERROR---------------------->\n\n', - match, + logger.debug( + '\n\n<----------------------[CustomOutputParser] HIT NO ACTION INPUT PARSING ERROR---------------------->\n\n' + + match, ); // In case there is no action input, let's double-check if there is an action input in 'text' variable @@ -139,7 +140,9 @@ class CustomOutputParser extends ZeroShotAgentOutputParser { } if (match && selectedTool.length > this.longestToolName.length) { - console.log('\n\n<----------------------HIT LONG PARSING ERROR---------------------->\n\n'); + logger.debug( + '\n\n<----------------------[CustomOutputParser] HIT LONG PARSING ERROR---------------------->\n\n', + ); let action, input, thought; let firstIndex = Infinity; @@ -156,9 +159,9 @@ class CustomOutputParser extends ZeroShotAgentOutputParser { // In case there is no action input, let's double-check if there is an action input in 'text' variable const actionInputMatch = this.actionInputRegex.exec(text); if (action && actionInputMatch) { - console.log( - '\n\n<------Matched Action Input in Long Parsing Error------>\n\n', - actionInputMatch, + logger.debug( + '\n\n<------[CustomOutputParser] Matched Action Input in Long Parsing Error------>\n\n' + + actionInputMatch, ); return { tool: action, @@ -185,15 +188,14 @@ class CustomOutputParser extends ZeroShotAgentOutputParser { const inputMatch = this.actionValues.exec(returnValues.log); //new if (inputMatch) { - console.log('inputMatch'); - console.dir(inputMatch, { depth: null }); + logger.debug('[CustomOutputParser] inputMatch', inputMatch); returnValues.toolInput = inputMatch[1].replaceAll('"', '').trim(); returnValues.log = returnValues.log.replace(this.actionValues, ''); } return returnValues; } else { - console.log('No valid tool mentioned.', this.tools, text); + logger.debug('[CustomOutputParser] No valid tool mentioned.', this.tools, text); return { tool: 'self-reflection', toolInput: 'Hypothetical actions: \n"' + text + '"\n', @@ -202,8 +204,8 @@ class CustomOutputParser extends ZeroShotAgentOutputParser { } // if (action && input) { - // console.log('Action:', action); - // console.log('Input:', input); + // logger.debug('Action:', action); + // logger.debug('Input:', input); // } } diff --git a/api/app/clients/agents/Functions/FunctionsAgent.js b/api/app/clients/agents/Functions/FunctionsAgent.js index 399d3f8473..476a6bda5c 100644 --- a/api/app/clients/agents/Functions/FunctionsAgent.js +++ b/api/app/clients/agents/Functions/FunctionsAgent.js @@ -7,6 +7,8 @@ const { SystemMessagePromptTemplate, HumanMessagePromptTemplate, } = require('langchain/prompts'); +const { logger } = require('~/config'); + const PREFIX = 'You are a helpful AI assistant.'; function parseOutput(message) { @@ -112,7 +114,7 @@ class FunctionsAgent extends Agent { valuesForLLM, callbackManager, ); - console.log('message', message); + logger.debug('[FunctionsAgent] plan message', message); return parseOutput(message); } } diff --git a/api/app/clients/callbacks/createStartHandler.js b/api/app/clients/callbacks/createStartHandler.js index 48d17c9adf..e7dfd15569 100644 --- a/api/app/clients/callbacks/createStartHandler.js +++ b/api/app/clients/callbacks/createStartHandler.js @@ -3,6 +3,7 @@ const { EModelEndpoint } = require('librechat-data-provider'); const { formatFromLangChain } = require('~/app/clients/prompts'); const checkBalance = require('~/models/checkBalance'); const { isEnabled } = require('~/server/utils'); +const { logger } = require('~/config'); const createStartHandler = ({ context, @@ -16,9 +17,15 @@ const createStartHandler = ({ const { model, functions, function_call } = invocation_params; const messages = _messages[0].map(formatFromLangChain); - if (manager.debug) { - console.log(`handleChatModelStart: ${context}`); - console.dir({ model, functions, function_call }, { depth: null }); + logger.debug(`[createStartHandler] handleChatModelStart: ${context}`, { + model, + function_call, + }); + + if (context !== 'title') { + logger.debug(`[createStartHandler] handleChatModelStart: ${context}`, { + functions, + }); } const payload = { messages }; @@ -35,9 +42,10 @@ const createStartHandler = ({ } prelimPromptTokens += promptTokensEstimate(payload); - if (manager.debug) { - console.log('Prelim Prompt Tokens & Token Buffer', prelimPromptTokens, tokenBuffer); - } + logger.debug('[createStartHandler]', { + prelimPromptTokens, + tokenBuffer, + }); prelimPromptTokens += tokenBuffer; try { @@ -61,7 +69,7 @@ const createStartHandler = ({ }); } } catch (err) { - console.error(`[${context}] checkBalance error`, err); + logger.error(`[createStartHandler][${context}] checkBalance error`, err); manager.abortController.abort(); if (context === 'summary' || context === 'plugins') { manager.addRun(runId, { conversationId, error: err.message }); diff --git a/api/app/clients/chains/runTitleChain.js b/api/app/clients/chains/runTitleChain.js index ec7b6e48c8..a020ffb8e3 100644 --- a/api/app/clients/chains/runTitleChain.js +++ b/api/app/clients/chains/runTitleChain.js @@ -1,6 +1,7 @@ const { z } = require('zod'); const { langPrompt, createTitlePrompt, escapeBraces, getSnippet } = require('../prompts'); const { createStructuredOutputChainFromZod } = require('langchain/chains/openai_functions'); +const { logger } = require('~/config'); const langSchema = z.object({ language: z.string().describe('The language of the input text (full noun, no abbreviations).'), @@ -30,8 +31,7 @@ const runTitleChain = async ({ llm, text, convo, signal, callbacks }) => { try { snippet = getSnippet(text); } catch (e) { - console.log('Error getting snippet of text for titleChain'); - console.log(e); + logger.error('[runTitleChain] Error getting snippet of text for titleChain', e); } const languageChain = createLanguageChain({ llm, callbacks }); const titleChain = createTitleChain({ llm, callbacks, convo: escapeBraces(convo) }); diff --git a/api/app/clients/llm/RunManager.js b/api/app/clients/llm/RunManager.js index 8e0219cae7..7ab0b06b52 100644 --- a/api/app/clients/llm/RunManager.js +++ b/api/app/clients/llm/RunManager.js @@ -1,5 +1,6 @@ -const { createStartHandler } = require('../callbacks'); -const spendTokens = require('../../../models/spendTokens'); +const { createStartHandler } = require('~/app/clients/callbacks'); +const spendTokens = require('~/models/spendTokens'); +const { logger } = require('~/config'); class RunManager { constructor(fields) { @@ -35,7 +36,7 @@ class RunManager { if (this.runs.has(runId)) { this.runs.delete(runId); } else { - console.error(`Run with ID ${runId} does not exist.`); + logger.error(`[api/app/clients/llm/RunManager] Run with ID ${runId} does not exist.`); } } @@ -57,10 +58,19 @@ class RunManager { { handleChatModelStart: createStartHandler({ ...metadata, manager: this }), handleLLMEnd: async (output, runId, _parentRunId) => { - if (this.debug) { - console.log(`handleLLMEnd: ${JSON.stringify(metadata)}`); - console.dir({ output, runId, _parentRunId }, { depth: null }); + const { llmOutput, ..._output } = output; + logger.debug(`[RunManager] handleLLMEnd: ${JSON.stringify(metadata)}`, { + runId, + _parentRunId, + llmOutput, + }); + + if (metadata.context !== 'title') { + logger.debug('[RunManager] handleLLMEnd:', { + output: _output, + }); } + const { tokenUsage } = output.llmOutput; const run = this.getRunById(runId); this.removeRun(runId); @@ -74,8 +84,7 @@ class RunManager { await spendTokens(txData, tokenUsage); }, handleLLMError: async (err) => { - this.debug && console.log(`handleLLMError: ${JSON.stringify(metadata)}`); - this.debug && console.error(err); + logger.error(`[RunManager] handleLLMError: ${JSON.stringify(metadata)}`, err); if (metadata.context === 'title') { return; } else if (metadata.context === 'plugins') { diff --git a/api/app/clients/memory/summaryBuffer.js b/api/app/clients/memory/summaryBuffer.js index eb36e71a57..0555fc214e 100644 --- a/api/app/clients/memory/summaryBuffer.js +++ b/api/app/clients/memory/summaryBuffer.js @@ -1,6 +1,7 @@ const { ConversationSummaryBufferMemory, ChatMessageHistory } = require('langchain/memory'); const { formatLangChainMessages, SUMMARY_PROMPT } = require('../prompts'); const { predictNewSummary } = require('../chains'); +const { logger } = require('~/config'); const createSummaryBufferMemory = ({ llm, prompt, messages, ...rest }) => { const chatHistory = new ChatMessageHistory(messages); @@ -22,9 +23,8 @@ const summaryBuffer = async ({ prompt = SUMMARY_PROMPT, signal, }) => { - if (debug && previous_summary) { - console.log('<-----------PREVIOUS SUMMARY----------->\n\n'); - console.log(previous_summary); + if (previous_summary) { + logger.debug('[summaryBuffer]', { previous_summary }); } const formattedMessages = formatLangChainMessages(context, formatOptions); @@ -46,8 +46,7 @@ const summaryBuffer = async ({ const messages = await chatPromptMemory.chatHistory.getMessages(); if (debug) { - console.log('<-----------SUMMARY BUFFER MESSAGES----------->\n\n'); - console.log(JSON.stringify(messages)); + logger.debug('[summaryBuffer]', { summary_buffer_messages: messages.length }); } const predictSummary = await predictNewSummary({ @@ -58,8 +57,7 @@ const summaryBuffer = async ({ }); if (debug) { - console.log('<-----------SUMMARY----------->\n\n'); - console.log(JSON.stringify(predictSummary)); + logger.debug('[summaryBuffer]', { summary: predictSummary }); } return { role: 'system', content: predictSummary }; diff --git a/api/app/clients/output_parsers/addImages.js b/api/app/clients/output_parsers/addImages.js index b64dc16d46..38ceb9a686 100644 --- a/api/app/clients/output_parsers/addImages.js +++ b/api/app/clients/output_parsers/addImages.js @@ -1,3 +1,5 @@ +const { logger } = require('~/config'); + /** * The `addImages` function corrects any erroneous image URLs in the `responseMessage.text` * and appends image observations from `intermediateSteps` if they are not already present. @@ -20,7 +22,7 @@ * * addImages(intermediateSteps, responseMessage); * - * console.log(responseMessage.text); + * logger.debug(responseMessage.text); * // Outputs: 'Some text with ![desc](/images/test.png)\n![desc](/images/test.png)' * * @returns {void} @@ -62,7 +64,7 @@ function addImages(intermediateSteps, responseMessage) { if (observedImagePath && !responseMessage.text.includes(observedImagePath[0])) { responseMessage.text += '\n' + observation; if (process.env.DEBUG_PLUGINS) { - console.debug('[addImages] added image from intermediateSteps'); + logger.debug('[addImages] added image from intermediateSteps:', observation); } } }); diff --git a/api/app/clients/tools/AIPluginTool.js b/api/app/clients/tools/AIPluginTool.js deleted file mode 100644 index b89d3f0be1..0000000000 --- a/api/app/clients/tools/AIPluginTool.js +++ /dev/null @@ -1,238 +0,0 @@ -const { Tool } = require('langchain/tools'); -const yaml = require('js-yaml'); - -/* -export interface AIPluginToolParams { - name: string; - description: string; - apiSpec: string; - openaiSpec: string; - model: BaseLanguageModel; -} - -export interface PathParameter { - name: string; - description: string; -} - -export interface Info { - title: string; - description: string; - version: string; -} -export interface PathMethod { - summary: string; - operationId: string; - parameters?: PathParameter[]; -} - -interface ApiSpec { - openapi: string; - info: Info; - paths: { [key: string]: { [key: string]: PathMethod } }; -} -*/ - -function isJson(str) { - try { - JSON.parse(str); - } catch (e) { - return false; - } - return true; -} - -function convertJsonToYamlIfApplicable(spec) { - if (isJson(spec)) { - const jsonData = JSON.parse(spec); - return yaml.dump(jsonData); - } - return spec; -} - -function extractShortVersion(openapiSpec) { - openapiSpec = convertJsonToYamlIfApplicable(openapiSpec); - try { - const fullApiSpec = yaml.load(openapiSpec); - const shortApiSpec = { - openapi: fullApiSpec.openapi, - info: fullApiSpec.info, - paths: {}, - }; - - for (let path in fullApiSpec.paths) { - shortApiSpec.paths[path] = {}; - for (let method in fullApiSpec.paths[path]) { - shortApiSpec.paths[path][method] = { - summary: fullApiSpec.paths[path][method].summary, - operationId: fullApiSpec.paths[path][method].operationId, - parameters: fullApiSpec.paths[path][method].parameters?.map((parameter) => ({ - name: parameter.name, - description: parameter.description, - })), - }; - } - } - - return yaml.dump(shortApiSpec); - } catch (e) { - console.log(e); - return ''; - } -} -function printOperationDetails(operationId, openapiSpec) { - openapiSpec = convertJsonToYamlIfApplicable(openapiSpec); - let returnText = ''; - try { - let doc = yaml.load(openapiSpec); - let servers = doc.servers; - let paths = doc.paths; - let components = doc.components; - - for (let path in paths) { - for (let method in paths[path]) { - let operation = paths[path][method]; - if (operation.operationId === operationId) { - returnText += `The API request to do for operationId "${operationId}" is:\n`; - returnText += `Method: ${method.toUpperCase()}\n`; - - let url = servers[0].url + path; - returnText += `Path: ${url}\n`; - - returnText += 'Parameters:\n'; - if (operation.parameters) { - for (let param of operation.parameters) { - let required = param.required ? '' : ' (optional),'; - returnText += `- ${param.name} (${param.in},${required} ${param.schema.type}): ${param.description}\n`; - } - } else { - returnText += ' None\n'; - } - returnText += '\n'; - - let responseSchema = operation.responses['200'].content['application/json'].schema; - - // Check if schema is a reference - if (responseSchema.$ref) { - // Extract schema name from reference - let schemaName = responseSchema.$ref.split('/').pop(); - // Look up schema in components - responseSchema = components.schemas[schemaName]; - } - - returnText += 'Response schema:\n'; - returnText += '- Type: ' + responseSchema.type + '\n'; - returnText += '- Additional properties:\n'; - returnText += ' - Type: ' + responseSchema.additionalProperties?.type + '\n'; - if (responseSchema.additionalProperties?.properties) { - returnText += ' - Properties:\n'; - for (let prop in responseSchema.additionalProperties.properties) { - returnText += ` - ${prop} (${responseSchema.additionalProperties.properties[prop].type}): Description not provided in OpenAPI spec\n`; - } - } - } - } - } - if (returnText === '') { - returnText += `No operation with operationId "${operationId}" found.`; - } - return returnText; - } catch (e) { - console.log(e); - return ''; - } -} - -class AIPluginTool extends Tool { - /* - private _name: string; - private _description: string; - apiSpec: string; - openaiSpec: string; - model: BaseLanguageModel; - */ - - get name() { - return this._name; - } - - get description() { - return this._description; - } - - constructor(params) { - super(); - this._name = params.name; - this._description = params.description; - this.apiSpec = params.apiSpec; - this.openaiSpec = params.openaiSpec; - this.model = params.model; - } - - async _call(input) { - let date = new Date(); - let fullDate = `Date: ${date.getDate()}/${ - date.getMonth() + 1 - }/${date.getFullYear()}, Time: ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; - const prompt = `${fullDate}\nQuestion: ${input} \n${this.apiSpec}.`; - console.log(prompt); - const gptResponse = await this.model.predict(prompt); - let operationId = gptResponse.match(/operationId: (.*)/)?.[1]; - if (!operationId) { - return 'No operationId found in the response'; - } - if (operationId == 'No API path found to answer the question') { - return 'No API path found to answer the question'; - } - - let openApiData = printOperationDetails(operationId, this.openaiSpec); - - return openApiData; - } - - static async fromPluginUrl(url, model) { - const aiPluginRes = await fetch(url, {}); - if (!aiPluginRes.ok) { - throw new Error(`Failed to fetch plugin from ${url} with status ${aiPluginRes.status}`); - } - const aiPluginJson = await aiPluginRes.json(); - const apiUrlRes = await fetch(aiPluginJson.api.url, {}); - if (!apiUrlRes.ok) { - throw new Error( - `Failed to fetch API spec from ${aiPluginJson.api.url} with status ${apiUrlRes.status}`, - ); - } - const apiUrlJson = await apiUrlRes.text(); - const shortApiSpec = extractShortVersion(apiUrlJson); - return new AIPluginTool({ - name: aiPluginJson.name_for_model.toLowerCase(), - description: `A \`tool\` to learn the API documentation for ${aiPluginJson.name_for_model.toLowerCase()}, after which you can use 'http_request' to make the actual API call. Short description of how to use the API's results: ${ - aiPluginJson.description_for_model - })`, - apiSpec: ` -As an AI, your task is to identify the operationId of the relevant API path based on the condensed OpenAPI specifications provided. - -Please note: - -1. Do not imagine URLs. Only use the information provided in the condensed OpenAPI specifications. - -2. Do not guess the operationId. Identify it strictly based on the API paths and their descriptions. - -Your output should only include: -- operationId: The operationId of the relevant API path - -If you cannot find a suitable API path based on the OpenAPI specifications, please answer only "operationId: No API path found to answer the question". - -Now, based on the question above and the condensed OpenAPI specifications given below, identify the operationId: - -\`\`\` -${shortApiSpec} -\`\`\` -`, - openaiSpec: apiUrlJson, - model: model, - }); - } -} - -module.exports = AIPluginTool; diff --git a/api/app/clients/tools/AzureAiSearch.js b/api/app/clients/tools/AzureAiSearch.js index 4f4b8a1ffe..2d74c00543 100644 --- a/api/app/clients/tools/AzureAiSearch.js +++ b/api/app/clients/tools/AzureAiSearch.js @@ -1,6 +1,7 @@ -const { StructuredTool } = require('langchain/tools'); const { z } = require('zod'); +const { StructuredTool } = require('langchain/tools'); const { SearchClient, AzureKeyCredential } = require('@azure/search-documents'); +const { logger } = require('~/config'); class AzureAISearch extends StructuredTool { // Constants for default values @@ -94,7 +95,7 @@ class AzureAISearch extends StructuredTool { } return JSON.stringify(resultDocuments); } catch (error) { - console.error(`Azure AI Search request failed: ${error.message}`); + logger.error('Azure AI Search request failed', error); return 'There was an error with Azure AI Search.'; } } diff --git a/api/app/clients/tools/DALL-E.js b/api/app/clients/tools/DALL-E.js index 505f570ace..88a7cf850a 100644 --- a/api/app/clients/tools/DALL-E.js +++ b/api/app/clients/tools/DALL-E.js @@ -3,13 +3,14 @@ const fs = require('fs'); const path = require('path'); const OpenAI = require('openai'); -// const { genAzureEndpoint } = require('../../../utils/genAzureEndpoints'); +// const { genAzureEndpoint } = require('~/utils/genAzureEndpoints'); const { Tool } = require('langchain/tools'); const { HttpsProxyAgent } = require('https-proxy-agent'); +const extractBaseURL = require('~/utils/extractBaseURL'); const saveImageFromUrl = require('./saveImageFromUrl'); -const extractBaseURL = require('../../../utils/extractBaseURL'); -const { DALLE_REVERSE_PROXY, PROXY } = process.env; +const { logger } = require('~/config'); +const { DALLE_REVERSE_PROXY, PROXY } = process.env; class OpenAICreateImage extends Tool { constructor(fields = {}) { super(); @@ -102,9 +103,12 @@ Guidelines: if (match) { imageName = match[0]; - console.log(imageName); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png + logger.debug('[DALL-E]', { imageName }); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png } else { - console.log('No image name found in the string.'); + logger.debug('[DALL-E] No image name found in the string.', { + theImageUrl, + data: resp.data[0], + }); } this.outputPath = path.resolve(__dirname, '..', '..', '..', '..', 'client', 'public', 'images'); @@ -120,7 +124,7 @@ Guidelines: await saveImageFromUrl(theImageUrl, this.outputPath, imageName); this.result = this.getMarkdownImageUrl(imageName); } catch (error) { - console.error('Error while saving the image:', error); + logger.error('Error while saving the DALL-E image:', error); this.result = theImageUrl; } diff --git a/api/app/clients/tools/GoogleSearch.js b/api/app/clients/tools/GoogleSearch.js index 3d782f164a..3d7574b6c1 100644 --- a/api/app/clients/tools/GoogleSearch.js +++ b/api/app/clients/tools/GoogleSearch.js @@ -1,5 +1,6 @@ -const { Tool } = require('langchain/tools'); const { google } = require('googleapis'); +const { Tool } = require('langchain/tools'); +const { logger } = require('~/config'); /** * Represents a tool that allows an agent to use the Google Custom Search API. @@ -86,7 +87,7 @@ class GoogleSearchAPI extends Tool { }); // return response.data; - // console.log(response.data); + // logger.debug(response.data); if (!response.data.items || response.data.items.length === 0) { return this.resultsToReadableFormat([ @@ -110,7 +111,7 @@ class GoogleSearchAPI extends Tool { return this.resultsToReadableFormat(metadataResults); } catch (error) { - console.log(`Error searching Google: ${error}`); + logger.error('[GoogleSearchAPI]', error); // throw error; return 'There was an error searching Google.'; } diff --git a/api/app/clients/tools/HttpRequestTool.js b/api/app/clients/tools/HttpRequestTool.js deleted file mode 100644 index a85e783b22..0000000000 --- a/api/app/clients/tools/HttpRequestTool.js +++ /dev/null @@ -1,108 +0,0 @@ -const { Tool } = require('langchain/tools'); - -// class RequestsGetTool extends Tool { -// constructor(headers = {}, { maxOutputLength } = {}) { -// super(); -// this.name = 'requests_get'; -// this.headers = headers; -// this.maxOutputLength = maxOutputLength || 2000; -// this.description = `A portal to the internet. Use this when you need to get specific content from a website. -// - Input should be a url (i.e. https://www.google.com). The output will be the text response of the GET request.`; -// } - -// async _call(input) { -// const res = await fetch(input, { -// headers: this.headers -// }); -// const text = await res.text(); -// return text.slice(0, this.maxOutputLength); -// } -// } - -// class RequestsPostTool extends Tool { -// constructor(headers = {}, { maxOutputLength } = {}) { -// super(); -// this.name = 'requests_post'; -// this.headers = headers; -// this.maxOutputLength = maxOutputLength || Infinity; -// this.description = `Use this when you want to POST to a website. -// - Input should be a json string with two keys: "url" and "data". -// - The value of "url" should be a string, and the value of "data" should be a dictionary of -// - key-value pairs you want to POST to the url as a JSON body. -// - Be careful to always use double quotes for strings in the json string -// - The output will be the text response of the POST request.`; -// } - -// async _call(input) { -// try { -// const { url, data } = JSON.parse(input); -// const res = await fetch(url, { -// method: 'POST', -// headers: this.headers, -// body: JSON.stringify(data) -// }); -// const text = await res.text(); -// return text.slice(0, this.maxOutputLength); -// } catch (error) { -// return `${error}`; -// } -// } -// } - -class HttpRequestTool extends Tool { - constructor(headers = {}, { maxOutputLength = Infinity } = {}) { - super(); - this.headers = headers; - this.name = 'http_request'; - this.maxOutputLength = maxOutputLength; - this.description = - 'Executes HTTP methods (GET, POST, PUT, DELETE, etc.). The input is an object with three keys: "url", "method", and "data". Even for GET or DELETE, include "data" key as an empty string. "method" is the HTTP method, and "url" is the desired endpoint. If POST or PUT, "data" should contain a stringified JSON representing the body to send. Only one url per use.'; - } - - async _call(input) { - try { - const urlPattern = /"url":\s*"([^"]*)"/; - const methodPattern = /"method":\s*"([^"]*)"/; - const dataPattern = /"data":\s*"([^"]*)"/; - - const url = input.match(urlPattern)[1]; - const method = input.match(methodPattern)[1]; - let data = input.match(dataPattern)[1]; - - // Parse 'data' back to JSON if possible - try { - data = JSON.parse(data); - } catch (e) { - // If it's not a JSON string, keep it as is - } - - let options = { - method: method, - headers: this.headers, - }; - - if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) && data) { - if (typeof data === 'object') { - options.body = JSON.stringify(data); - } else { - options.body = data; - } - options.headers['Content-Type'] = 'application/json'; - } - - const res = await fetch(url, options); - - const text = await res.text(); - if (text.includes(' 0) { - verbose && console.debug('headers detected', headers); + logger.debug('[createOpenAPIPlugin] headers detected', headers); chainOptions.headers = headers; } if (data.params) { - verbose && console.debug('params detected', data.params); + logger.debug('[createOpenAPIPlugin] params detected', data.params); chainOptions.params = data.params; } let history = ''; if (memory) { - verbose && console.debug('openAPI chain: memory detected', memory); + logger.debug('[createOpenAPIPlugin] openAPI chain: memory detected', memory); const { history: chat_history } = await memory.loadMemoryVariables({}); history = chat_history?.length > 0 ? `\n\n## Chat History:\n${chat_history}\n` : ''; } diff --git a/api/app/clients/tools/index.js b/api/app/clients/tools/index.js index ad8b61a7fc..f5410e89ee 100644 --- a/api/app/clients/tools/index.js +++ b/api/app/clients/tools/index.js @@ -1,6 +1,4 @@ const GoogleSearchAPI = require('./GoogleSearch'); -const HttpRequestTool = require('./HttpRequestTool'); -const AIPluginTool = require('./AIPluginTool'); const OpenAICreateImage = require('./DALL-E'); const DALLE3 = require('./structured/DALLE3'); const StructuredSD = require('./structured/StableDiffusion'); @@ -20,8 +18,6 @@ const CodeBrew = require('./CodeBrew'); module.exports = { availableTools, GoogleSearchAPI, - HttpRequestTool, - AIPluginTool, OpenAICreateImage, DALLE3, StableDiffusionAPI, diff --git a/api/app/clients/tools/saveImageFromUrl.js b/api/app/clients/tools/saveImageFromUrl.js index e67f532cdf..d8b14ad478 100644 --- a/api/app/clients/tools/saveImageFromUrl.js +++ b/api/app/clients/tools/saveImageFromUrl.js @@ -1,6 +1,7 @@ -const axios = require('axios'); const fs = require('fs'); const path = require('path'); +const axios = require('axios'); +const { logger } = require('~/config'); async function saveImageFromUrl(url, outputPath, outputFilename) { try { @@ -32,7 +33,7 @@ async function saveImageFromUrl(url, outputPath, outputFilename) { writer.on('error', reject); }); } catch (error) { - console.error('Error while saving the image:', error); + logger.error('[saveImageFromUrl] Error while saving the image:', error); } } diff --git a/api/app/clients/tools/structured/AzureAISearch.js b/api/app/clients/tools/structured/AzureAISearch.js index 4f4b8a1ffe..2d74c00543 100644 --- a/api/app/clients/tools/structured/AzureAISearch.js +++ b/api/app/clients/tools/structured/AzureAISearch.js @@ -1,6 +1,7 @@ -const { StructuredTool } = require('langchain/tools'); const { z } = require('zod'); +const { StructuredTool } = require('langchain/tools'); const { SearchClient, AzureKeyCredential } = require('@azure/search-documents'); +const { logger } = require('~/config'); class AzureAISearch extends StructuredTool { // Constants for default values @@ -94,7 +95,7 @@ class AzureAISearch extends StructuredTool { } return JSON.stringify(resultDocuments); } catch (error) { - console.error(`Azure AI Search request failed: ${error.message}`); + logger.error('Azure AI Search request failed', error); return 'There was an error with Azure AI Search.'; } } diff --git a/api/app/clients/tools/structured/CodeSherpa.js b/api/app/clients/tools/structured/CodeSherpa.js index ebfe5129e1..66311fca22 100644 --- a/api/app/clients/tools/structured/CodeSherpa.js +++ b/api/app/clients/tools/structured/CodeSherpa.js @@ -28,14 +28,14 @@ class RunCode extends StructuredTool { } async _call({ code, language = 'python' }) { - // console.log('<--------------- Running Code --------------->', { code, language }); + // logger.debug('<--------------- Running Code --------------->', { code, language }); const response = await axios({ url: `${this.url}/repl`, method: 'post', headers: this.headers, data: { code, language }, }); - // console.log('<--------------- Sucessfully ran Code --------------->', response.data); + // logger.debug('<--------------- Sucessfully ran Code --------------->', response.data); return response.data.result; } } diff --git a/api/app/clients/tools/structured/CodeSherpaTools.js b/api/app/clients/tools/structured/CodeSherpaTools.js index 49c9a8c915..4d1ab9805f 100644 --- a/api/app/clients/tools/structured/CodeSherpaTools.js +++ b/api/app/clients/tools/structured/CodeSherpaTools.js @@ -42,14 +42,14 @@ class RunCode extends StructuredTool { } async _call({ code, language = 'python' }) { - // console.log('<--------------- Running Code --------------->', { code, language }); + // logger.debug('<--------------- Running Code --------------->', { code, language }); const response = await axios({ url: `${this.url}/repl`, method: 'post', headers: this.headers, data: { code, language }, }); - // console.log('<--------------- Sucessfully ran Code --------------->', response.data); + // logger.debug('<--------------- Sucessfully ran Code --------------->', response.data); return response.data.result; } } diff --git a/api/app/clients/tools/structured/DALLE3.js b/api/app/clients/tools/structured/DALLE3.js index d868190d0a..dc5750a689 100644 --- a/api/app/clients/tools/structured/DALLE3.js +++ b/api/app/clients/tools/structured/DALLE3.js @@ -7,7 +7,9 @@ const OpenAI = require('openai'); const { Tool } = require('langchain/tools'); const { HttpsProxyAgent } = require('https-proxy-agent'); const saveImageFromUrl = require('../saveImageFromUrl'); -const extractBaseURL = require('../../../../utils/extractBaseURL'); +const extractBaseURL = require('~/utils/extractBaseURL'); +const { logger } = require('~/config'); + const { DALLE3_SYSTEM_PROMPT, DALLE_REVERSE_PROXY, PROXY } = process.env; class DALLE3 extends Tool { constructor(fields = {}) { @@ -126,9 +128,12 @@ Error Message: ${error.message}`; if (match) { imageName = match[0]; - console.log(imageName); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png + logger.debug('[DALL-E-3]', { imageName }); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png } else { - console.log('No image name found in the string.'); + logger.debug('[DALL-E-3] No image name found in the string.', { + theImageUrl, + data: resp.data[0], + }); } this.outputPath = path.resolve( @@ -154,7 +159,7 @@ Error Message: ${error.message}`; await saveImageFromUrl(theImageUrl, this.outputPath, imageName); this.result = this.getMarkdownImageUrl(imageName); } catch (error) { - console.error('Error while saving the image:', error); + logger.error('Error while saving the image:', error); this.result = theImageUrl; } diff --git a/api/app/clients/tools/structured/E2BTools.js b/api/app/clients/tools/structured/E2BTools.js index fc5fd6032f..7e6148008c 100644 --- a/api/app/clients/tools/structured/E2BTools.js +++ b/api/app/clients/tools/structured/E2BTools.js @@ -1,9 +1,10 @@ +const { z } = require('zod'); +const axios = require('axios'); const { StructuredTool } = require('langchain/tools'); const { PromptTemplate } = require('langchain/prompts'); -const { createExtractionChainFromZod } = require('./extractionChain'); // const { ChatOpenAI } = require('langchain/chat_models/openai'); -const axios = require('axios'); -const { z } = require('zod'); +const { createExtractionChainFromZod } = require('./extractionChain'); +const { logger } = require('~/config'); const envs = ['Nodejs', 'Go', 'Bash', 'Rust', 'Python3', 'PHP', 'Java', 'Perl', 'DotNET']; const env = z.enum(envs); @@ -34,8 +35,8 @@ async function extractEnvFromCode(code, model) { // const chatModel = new ChatOpenAI({ openAIApiKey, modelName: 'gpt-4-0613', temperature: 0 }); const chain = createExtractionChainFromZod(zodSchema, model, { prompt, verbose: true }); const result = await chain.run(code); - console.log('<--------------- extractEnvFromCode --------------->'); - console.log(result); + logger.debug('<--------------- extractEnvFromCode --------------->'); + logger.debug(result); return result.env; } @@ -69,7 +70,7 @@ class RunCommand extends StructuredTool { } async _call(data) { - console.log(`<--------------- Running ${data} --------------->`); + logger.debug(`<--------------- Running ${data} --------------->`); const response = await axios({ url: `${this.url}/commands`, method: 'post', @@ -96,7 +97,7 @@ class ReadFile extends StructuredTool { } async _call(data) { - console.log(`<--------------- Reading ${data} --------------->`); + logger.debug(`<--------------- Reading ${data} --------------->`); const response = await axios.get(`${this.url}/files`, { params: data, headers: this.headers }); return response.data; } @@ -121,12 +122,12 @@ class WriteFile extends StructuredTool { async _call(data) { let { env, path, content } = data; - console.log(`<--------------- environment ${env} typeof ${typeof env}--------------->`); + logger.debug(`<--------------- environment ${env} typeof ${typeof env}--------------->`); if (env && !envs.includes(env)) { - console.log(`<--------------- Invalid environment ${env} --------------->`); + logger.debug(`<--------------- Invalid environment ${env} --------------->`); env = await extractEnvFromCode(content, this.model); } else if (!env) { - console.log('<--------------- Undefined environment --------------->'); + logger.debug('<--------------- Undefined environment --------------->'); env = await extractEnvFromCode(content, this.model); } @@ -139,7 +140,7 @@ class WriteFile extends StructuredTool { content, }, }; - console.log('Writing to file', JSON.stringify(payload)); + logger.debug('Writing to file', JSON.stringify(payload)); await axios({ url: `${this.url}/files`, diff --git a/api/app/clients/tools/structured/StableDiffusion.js b/api/app/clients/tools/structured/StableDiffusion.js index c4c32cd3c0..1fc5096730 100644 --- a/api/app/clients/tools/structured/StableDiffusion.js +++ b/api/app/clients/tools/structured/StableDiffusion.js @@ -1,10 +1,11 @@ // Generates image using stable diffusion webui's api (automatic1111) const fs = require('fs'); -const { StructuredTool } = require('langchain/tools'); const { z } = require('zod'); const path = require('path'); const axios = require('axios'); const sharp = require('sharp'); +const { StructuredTool } = require('langchain/tools'); +const { logger } = require('~/config'); class StableDiffusionAPI extends StructuredTool { constructor(fields) { @@ -107,7 +108,7 @@ class StableDiffusionAPI extends StructuredTool { .toFile(this.outputPath + '/' + imageName); this.result = this.getMarkdownImageUrl(imageName); } catch (error) { - console.error('Error while saving the image:', error); + logger.error('[StableDiffusion] Error while saving the image:', error); // this.result = theImageUrl; } diff --git a/api/app/clients/tools/structured/Wolfram.js b/api/app/clients/tools/structured/Wolfram.js index dadd2048ae..2c5c6e023a 100644 --- a/api/app/clients/tools/structured/Wolfram.js +++ b/api/app/clients/tools/structured/Wolfram.js @@ -1,7 +1,8 @@ /* eslint-disable no-useless-escape */ const axios = require('axios'); -const { StructuredTool } = require('langchain/tools'); const { z } = require('zod'); +const { StructuredTool } = require('langchain/tools'); +const { logger } = require('~/config'); class WolframAlphaAPI extends StructuredTool { constructor(fields) { @@ -47,7 +48,7 @@ class WolframAlphaAPI extends StructuredTool { const response = await axios.get(url, { responseType: 'text' }); return response.data; } catch (error) { - console.error(`Error fetching raw text: ${error}`); + logger.error('[WolframAlphaAPI] Error fetching raw text:', error); throw error; } } @@ -78,11 +79,10 @@ class WolframAlphaAPI extends StructuredTool { return response; } catch (error) { if (error.response && error.response.data) { - console.log('Error data:', error.response.data); + logger.error('[WolframAlphaAPI] Error data:', error); return error.response.data; } else { - console.log('Error querying Wolfram Alpha', error.message); - // throw error; + logger.error('[WolframAlphaAPI] Error querying Wolfram Alpha', error); return 'There was an error querying Wolfram Alpha.'; } } diff --git a/api/app/clients/tools/structured/specs/DALLE3.spec.js b/api/app/clients/tools/structured/specs/DALLE3.spec.js index 0958886df7..c61e1d3513 100644 --- a/api/app/clients/tools/structured/specs/DALLE3.spec.js +++ b/api/app/clients/tools/structured/specs/DALLE3.spec.js @@ -3,6 +3,7 @@ const path = require('path'); const OpenAI = require('openai'); const DALLE3 = require('../DALLE3'); const saveImageFromUrl = require('../../saveImageFromUrl'); +const { logger } = require('~/config'); jest.mock('openai'); @@ -145,10 +146,13 @@ describe('DALLE3', () => { }, ], }; - console.log = jest.fn(); // Mock console.log + generate.mockResolvedValue(mockResponse); await dalle._call(mockData); - expect(console.log).toHaveBeenCalledWith('No image name found in the string.'); + expect(logger.debug).toHaveBeenCalledWith('[DALL-E-3] No image name found in the string.', { + data: { url: 'http://example.com/invalid-url' }, + theImageUrl: 'http://example.com/invalid-url', + }); }); it('should create the directory if it does not exist', async () => { @@ -182,9 +186,8 @@ describe('DALLE3', () => { const error = new Error('Error while saving the image'); generate.mockResolvedValue(mockResponse); saveImageFromUrl.mockRejectedValue(error); - console.error = jest.fn(); // Mock console.error const result = await dalle._call(mockData); - expect(console.error).toHaveBeenCalledWith('Error while saving the image:', error); + expect(logger.error).toHaveBeenCalledWith('Error while saving the image:', error); expect(result).toBe(mockResponse.data[0].url); }); }); diff --git a/api/app/clients/tools/util/handleOpenAIErrors.js b/api/app/clients/tools/util/handleOpenAIErrors.js index b5a31f7f40..53a4f37ace 100644 --- a/api/app/clients/tools/util/handleOpenAIErrors.js +++ b/api/app/clients/tools/util/handleOpenAIErrors.js @@ -1,4 +1,5 @@ const OpenAI = require('openai'); +const { logger } = require('~/config'); /** * Handles errors that may occur when making requests to OpenAI's API. @@ -12,14 +13,14 @@ const OpenAI = require('openai'); */ async function handleOpenAIErrors(err, errorCallback, context = 'stream') { if (err instanceof OpenAI.APIError && err?.message?.includes('abort')) { - console.warn(`[OpenAIClient.chatCompletion][${context}] Aborted Message`); + logger.warn(`[OpenAIClient.chatCompletion][${context}] Aborted Message`); } if (err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason')) { - console.warn(`[OpenAIClient.chatCompletion][${context}] Missing finish_reason`); + logger.warn(`[OpenAIClient.chatCompletion][${context}] Missing finish_reason`); } else if (err instanceof OpenAI.APIError) { - console.warn(`[OpenAIClient.chatCompletion][${context}] API Error`); + logger.warn(`[OpenAIClient.chatCompletion][${context}] API error`); } else { - console.warn(`[OpenAIClient.chatCompletion][${context}] Unhandled error type`); + logger.warn(`[OpenAIClient.chatCompletion][${context}] Unhandled error type`); } if (errorCallback) { diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index 6109e2b9c2..3afe277672 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -1,17 +1,14 @@ -const { getUserPluginAuthValue } = require('../../../../server/services/PluginService'); -const { OpenAIEmbeddings } = require('langchain/embeddings/openai'); const { ZapierToolKit } = require('langchain/agents'); -const { SerpAPI, ZapierNLAWrapper } = require('langchain/tools'); -const { ChatOpenAI } = require('langchain/chat_models/openai'); const { Calculator } = require('langchain/tools/calculator'); const { WebBrowser } = require('langchain/tools/webbrowser'); +const { SerpAPI, ZapierNLAWrapper } = require('langchain/tools'); +const { OpenAIEmbeddings } = require('langchain/embeddings/openai'); +const { getUserPluginAuthValue } = require('~/server/services/PluginService'); const { availableTools, - AIPluginTool, GoogleSearchAPI, WolframAlphaAPI, StructuredWolfram, - HttpRequestTool, OpenAICreateImage, StableDiffusionAPI, DALLE3, @@ -23,8 +20,9 @@ const { CodeSherpaTools, CodeBrew, } = require('../'); -const { loadSpecs } = require('./loadSpecs'); const { loadToolSuite } = require('./loadToolSuite'); +const { loadSpecs } = require('./loadSpecs'); +const { logger } = require('~/config'); const getOpenAIKey = async (options, user) => { let openAIApiKey = options.openAIApiKey ?? process.env.OPENAI_API_KEY; @@ -64,7 +62,7 @@ const validateTools = async (user, tools = []) => { return Array.from(validToolsSet.values()); } catch (err) { - console.log('There was a problem validating tools', err); + logger.error('[validateTools] There was a problem validating tools', err); throw new Error(err); } }; @@ -161,15 +159,6 @@ const loadTools = async ({ const zapier = new ZapierNLAWrapper({ apiKey }); return ZapierToolKit.fromZapierNLAWrapper(zapier); }, - plugins: async () => { - return [ - new HttpRequestTool(), - await AIPluginTool.fromPluginUrl( - 'https://www.klarna.com/.well-known/ai-plugin.json', - new ChatOpenAI({ openAIApiKey: options.openAIApiKey, temperature: 0 }), - ), - ]; - }, }; const requestedTools = {}; diff --git a/api/app/clients/tools/util/loadSpecs.js b/api/app/clients/tools/util/loadSpecs.js index da787c6094..e5b543132a 100644 --- a/api/app/clients/tools/util/loadSpecs.js +++ b/api/app/clients/tools/util/loadSpecs.js @@ -1,7 +1,8 @@ const fs = require('fs'); const path = require('path'); const { z } = require('zod'); -const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin'); +const { logger } = require('~/config'); +const { createOpenAPIPlugin } = require('~/app/clients/tools/dynamic/OpenAPIPlugin'); // The minimum Manifest definition const ManifestDefinition = z.object({ @@ -26,28 +27,17 @@ const ManifestDefinition = z.object({ legal_info_url: z.string().optional(), }); -function validateJson(json, verbose = true) { +function validateJson(json) { try { return ManifestDefinition.parse(json); } catch (error) { - if (verbose) { - console.debug('validateJson error', error); - } + logger.debug('[validateJson] manifest parsing error', error); return false; } } // omit the LLM to return the well known jsons as objects -async function loadSpecs({ - llm, - user, - message, - tools = [], - map = false, - memory, - signal, - verbose = false, -}) { +async function loadSpecs({ llm, user, message, tools = [], map = false, memory, signal }) { const directoryPath = path.join(__dirname, '..', '.well-known'); let files = []; @@ -60,7 +50,7 @@ async function loadSpecs({ await fs.promises.access(filePath, fs.constants.F_OK); files.push(tools[i] + '.json'); } catch (err) { - console.error(`File ${tools[i] + '.json'} does not exist`); + logger.error(`[loadSpecs] File ${tools[i] + '.json'} does not exist`, err); } } @@ -73,9 +63,7 @@ async function loadSpecs({ const validJsons = []; const constructorMap = {}; - if (verbose) { - console.debug('files', files); - } + logger.debug('[validateJson] files', files); for (const file of files) { if (path.extname(file) === '.json') { @@ -84,7 +72,7 @@ async function loadSpecs({ const json = JSON.parse(fileContent); if (!validateJson(json)) { - verbose && console.debug('Invalid json', json); + logger.debug('[validateJson] Invalid json', json); continue; } @@ -97,13 +85,12 @@ async function loadSpecs({ memory, signal, user, - verbose, }); continue; } if (llm) { - validJsons.push(createOpenAPIPlugin({ data: json, llm, verbose })); + validJsons.push(createOpenAPIPlugin({ data: json, llm })); continue; } @@ -117,10 +104,8 @@ async function loadSpecs({ const plugins = (await Promise.all(validJsons)).filter((plugin) => plugin); - // if (verbose) { - // console.debug('plugins', plugins); - // console.debug(plugins[0].name); - // } + // logger.debug('[validateJson] plugins', plugins); + // logger.debug(plugins[0].name); return plugins; } diff --git a/api/app/titleConvoBing.js b/api/app/titleConvoBing.js index 8dd32160d7..7c5c7e2c61 100644 --- a/api/app/titleConvoBing.js +++ b/api/app/titleConvoBing.js @@ -1,5 +1,6 @@ -const { isEnabled } = require('../server/utils'); const throttle = require('lodash/throttle'); +const { isEnabled } = require('~/server/utils'); +const { logger } = require('~/config'); const titleConvo = async ({ text, response }) => { let title = 'New Chat'; @@ -30,11 +31,10 @@ const titleConvo = async ({ text, response }) => { const res = await titleGenerator.sendMessage(titlePrompt, options); title = res.response.replace(/Title: /, '').replace(/[".]/g, ''); } catch (e) { - console.error(e); - console.log('There was an issue generating title, see error above'); + logger.error('There was an issue generating title with BingAI', e); } - console.log('CONVERSATION TITLE', title); + logger.debug('[/ask/bingAI] CONVERSATION TITLE: ' + title); return title; }; diff --git a/api/cache/banViolation.js b/api/cache/banViolation.js index e0ae26319e..3d67e57872 100644 --- a/api/cache/banViolation.js +++ b/api/cache/banViolation.js @@ -1,6 +1,8 @@ -const Session = require('../models/Session'); +const Session = require('~/models/Session'); const getLogStores = require('./getLogStores'); -const { isEnabled, math, removePorts } = require('../server/utils'); +const { isEnabled, math, removePorts } = require('~/server/utils'); +const { logger } = require('~/config'); + const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {}; const interval = math(BAN_INTERVAL, 20); @@ -54,7 +56,7 @@ const banViolation = async (req, res, errorMessage) => { } req.ip = removePorts(req); - console.log( + logger.info( `[BAN] Banning user ${user_id} ${req.ip ? `@ ${req.ip} ` : ''}for ${ duration / 1000 / 60 } minutes`, diff --git a/api/cache/keyvMongo.js b/api/cache/keyvMongo.js index 429329adc6..8f5b9fd8d8 100644 --- a/api/cache/keyvMongo.js +++ b/api/cache/keyvMongo.js @@ -1,7 +1,9 @@ const KeyvMongo = require('@keyv/mongo'); +const { logger } = require('~/config'); + const { MONGO_URI } = process.env ?? {}; const keyvMongo = new KeyvMongo(MONGO_URI, { collection: 'logs' }); -keyvMongo.on('error', (err) => console.error('KeyvMongo connection error:', err)); +keyvMongo.on('error', (err) => logger.error('KeyvMongo connection error:', err)); module.exports = keyvMongo; diff --git a/api/cache/keyvRedis.js b/api/cache/keyvRedis.js index 942b1b239f..a5cbb45f11 100644 --- a/api/cache/keyvRedis.js +++ b/api/cache/keyvRedis.js @@ -1,4 +1,5 @@ const KeyvRedis = require('@keyv/redis'); +const { logger } = require('~/config'); const { REDIS_URI } = process.env; @@ -6,9 +7,9 @@ let keyvRedis; if (REDIS_URI) { keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false }); - keyvRedis.on('error', (err) => console.error('KeyvRedis connection error:', err)); + keyvRedis.on('error', (err) => logger.error('KeyvRedis connection error:', err)); } else { - // console.log('REDIS_URI not provided. Redis module will not be initialized.'); + logger.info('REDIS_URI not provided. Redis module will not be initialized.'); } module.exports = keyvRedis; diff --git a/api/common/enums.js b/api/common/enums.js index dc1c757b2f..849ae43f59 100644 --- a/api/common/enums.js +++ b/api/common/enums.js @@ -1,12 +1,14 @@ /** * @typedef {Object} CacheKeys * @property {'config'} CONFIG - Key for the config cache. + * @property {'plugins'} PLUGINS - Key for the plugins cache. * @property {'modelsConfig'} MODELS_CONFIG - Key for the model config cache. * @property {'defaultConfig'} DEFAULT_CONFIG - Key for the default config cache. * @property {'overrideConfig'} OVERRIDE_CONFIG - Key for the override config cache. */ const CacheKeys = { CONFIG: 'config', + PLUGINS: 'plugins', MODELS_CONFIG: 'modelsConfig', DEFAULT_CONFIG: 'defaultConfig', OVERRIDE_CONFIG: 'overrideConfig', diff --git a/api/config.js b/api/config.js deleted file mode 100644 index a17b607490..0000000000 --- a/api/config.js +++ /dev/null @@ -1,6 +0,0 @@ -const path = require('path'); - -module.exports = { - publicPath: path.resolve(__dirname, '..', 'client', 'public'), - imageOutput: path.resolve(__dirname, '..', 'client', 'public', 'images'), -}; diff --git a/api/config/index.js b/api/config/index.js new file mode 100644 index 0000000000..3198ff2fb2 --- /dev/null +++ b/api/config/index.js @@ -0,0 +1,5 @@ +const logger = require('./winston'); + +module.exports = { + logger, +}; diff --git a/api/config/meiliLogger.js b/api/config/meiliLogger.js new file mode 100644 index 0000000000..195b387ae5 --- /dev/null +++ b/api/config/meiliLogger.js @@ -0,0 +1,78 @@ +const path = require('path'); +const winston = require('winston'); +require('winston-daily-rotate-file'); + +const logDir = path.join(__dirname, '..', 'logs'); + +const { NODE_ENV } = process.env; + +const levels = { + error: 0, + warn: 1, + info: 2, + http: 3, + verbose: 4, + debug: 5, + activity: 6, + silly: 7, +}; + +winston.addColors({ + info: 'green', // fontStyle color + warn: 'italic yellow', + error: 'red', + debug: 'blue', +}); + +const level = () => { + const env = NODE_ENV || 'development'; + const isDevelopment = env === 'development'; + return isDevelopment ? 'debug' : 'warn'; +}; + +const fileFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.errors({ stack: true }), + winston.format.splat(), +); + +const transports = [ + new winston.transports.DailyRotateFile({ + level: 'debug', + filename: `${logDir}/meiliSync-%DATE%.log`, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d', + format: fileFormat, + }), +]; + +// if (NODE_ENV !== 'production') { +// transports.push( +// new winston.transports.Console({ +// format: winston.format.combine(winston.format.colorize(), winston.format.simple()), +// }), +// ); +// } + +const consoleFormat = winston.format.combine( + winston.format.colorize({ all: true }), + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), +); + +transports.push( + new winston.transports.Console({ + level: 'info', + format: consoleFormat, + }), +); + +const logger = winston.createLogger({ + level: level(), + levels, + transports, +}); + +module.exports = logger; diff --git a/api/config/parsers.js b/api/config/parsers.js new file mode 100644 index 0000000000..0d842f9c29 --- /dev/null +++ b/api/config/parsers.js @@ -0,0 +1,128 @@ +const util = require('util'); +const winston = require('winston'); +const traverse = require('traverse'); +const { klona } = require('klona/full'); + +const sensitiveKeys = [/^sk-\w+$/]; + +/** + * Determines if a given key string is sensitive. + * + * @param {string} keyStr - The key string to check. + * @returns {boolean} True if the key string matches known sensitive key patterns. + */ +function isSensitiveKey(keyStr) { + if (keyStr) { + return sensitiveKeys.some((regex) => regex.test(keyStr)); + } + return false; +} + +/** + * Recursively redacts sensitive information from an object. + * + * @param {object} obj - The object to traverse and redact. + */ +function redactObject(obj) { + traverse(obj).forEach(function redactor() { + if (isSensitiveKey(this.key)) { + this.update('[REDACTED]'); + } + }); +} + +/** + * Deep copies and redacts sensitive information from an object. + * + * @param {object} obj - The object to copy and redact. + * @returns {object} The redacted copy of the original object. + */ +function redact(obj) { + const copy = klona(obj); // Making a deep copy to prevent side effects + redactObject(copy); + + const splat = copy[Symbol.for('splat')]; + redactObject(splat); // Specifically redact splat Symbol + + return copy; +} + +/** + * Truncates long strings, especially base64 image data, within log messages. + * + * @param {any} value - The value to be inspected and potentially truncated. + * @returns {any} - The truncated or original value. + */ +const truncateLongStrings = (value) => { + if (typeof value === 'string') { + return value.length > 100 ? value.substring(0, 100) + '... [truncated]' : value; + } + + return value; +}; + +// /** +// * Processes each message in the messages array, specifically looking for and truncating +// * base64 image URLs in the content. If a base64 image URL is found, it replaces the URL +// * with a truncated message. +// * +// * @param {PayloadMessage} message - The payload message object to format. +// * @returns {PayloadMessage} - The processed message object with base64 image URLs truncated. +// */ +// const truncateBase64ImageURLs = (message) => { +// // Create a deep copy of the message +// const messageCopy = JSON.parse(JSON.stringify(message)); + +// if (messageCopy.content && Array.isArray(messageCopy.content)) { +// messageCopy.content = messageCopy.content.map(contentItem => { +// if (contentItem.type === 'image_url' && contentItem.image_url && isBase64String(contentItem.image_url.url)) { +// return { ...contentItem, image_url: { ...contentItem.image_url, url: 'Base64 Image Data... [truncated]' } }; +// } +// return contentItem; +// }); +// } +// return messageCopy; +// }; + +// /** +// * Checks if a string is a base64 image data string. +// * +// * @param {string} str - The string to be checked. +// * @returns {boolean} - True if the string is base64 image data, otherwise false. +// */ +// const isBase64String = (str) => /^data:image\/[a-zA-Z]+;base64,/.test(str); + +/** + * Custom log format for Winston that handles deep object inspection. + * It specifically truncates long strings and handles nested structures within metadata. + * + * @param {Object} info - Information about the log entry. + * @returns {string} - The formatted log message. + */ +const deepObjectFormat = winston.format.printf(({ level, message, timestamp, ...metadata }) => { + let msg = `${timestamp} ${level}: ${message}`; + + if (Object.keys(metadata).length) { + Object.entries(metadata).forEach(([key, value]) => { + let val = value; + if (key === 'modelOptions' && value && Array.isArray(value.messages)) { + // Create a shallow copy of the messages array + // val = { ...value, messages: value.messages.map(truncateBase64ImageURLs) }; + val = { ...value, messages: `${value.messages.length} message(s) in payload` }; + } + // Inspects each metadata value; applies special handling for 'messages' + const inspectedValue = + typeof val === 'string' + ? truncateLongStrings(val) + : util.inspect(val, { depth: null, colors: false }); // Use 'val' here + msg += ` ${key}: ${inspectedValue}`; + }); + } + + return msg; +}); + +module.exports = { + redact, + deepObjectFormat, +}; diff --git a/api/config/paths.js b/api/config/paths.js new file mode 100644 index 0000000000..2f577a183f --- /dev/null +++ b/api/config/paths.js @@ -0,0 +1,6 @@ +const path = require('path'); + +module.exports = { + publicPath: path.resolve(__dirname, '..', '..', 'client', 'public'), + imageOutput: path.resolve(__dirname, '..', '..', 'client', 'public', 'images'), +}; diff --git a/api/config/winston.js b/api/config/winston.js new file mode 100644 index 0000000000..d689cee8bd --- /dev/null +++ b/api/config/winston.js @@ -0,0 +1,113 @@ +const path = require('path'); +const winston = require('winston'); +require('winston-daily-rotate-file'); +const { redact, deepObjectFormat } = require('./parsers'); +const { isEnabled } = require('~/server/utils/handleText'); + +const logDir = path.join(__dirname, '..', 'logs'); + +const { NODE_ENV, DEBUG_LOGGING = true, DEBUG_CONSOLE = false } = process.env; + +const levels = { + error: 0, + warn: 1, + info: 2, + http: 3, + verbose: 4, + debug: 5, + activity: 6, + silly: 7, +}; + +winston.addColors({ + info: 'green', // fontStyle color + warn: 'italic yellow', + error: 'red', + debug: 'blue', +}); + +const level = () => { + const env = NODE_ENV || 'development'; + const isDevelopment = env === 'development'; + return isDevelopment ? 'debug' : 'warn'; +}; + +const fileFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.errors({ stack: true }), + winston.format.splat(), + winston.format((info) => redact(info))(), +); + +const transports = [ + new winston.transports.DailyRotateFile({ + level: 'error', + filename: `${logDir}/error-%DATE%.log`, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d', + format: fileFormat, + }), + // new winston.transports.DailyRotateFile({ + // level: 'info', + // filename: `${logDir}/info-%DATE%.log`, + // datePattern: 'YYYY-MM-DD', + // zippedArchive: true, + // maxSize: '20m', + // maxFiles: '14d', + // }), +]; + +// if (NODE_ENV !== 'production') { +// transports.push( +// new winston.transports.Console({ +// format: winston.format.combine(winston.format.colorize(), winston.format.simple()), +// }), +// ); +// } + +if (isEnabled && isEnabled(DEBUG_LOGGING)) { + transports.push( + new winston.transports.DailyRotateFile({ + level: 'debug', + filename: `${logDir}/debug-%DATE%.log`, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d', + format: winston.format.combine(fileFormat, deepObjectFormat), + }), + ); +} + +const consoleFormat = winston.format.combine( + winston.format.colorize({ all: true }), + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format((info) => redact(info))(), + winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), +); + +if (isEnabled && isEnabled(DEBUG_CONSOLE)) { + transports.push( + new winston.transports.Console({ + level: 'debug', + format: winston.format.combine(consoleFormat, deepObjectFormat), + }), + ); +} else { + transports.push( + new winston.transports.Console({ + level: 'info', + format: consoleFormat, + }), + ); +} + +const logger = winston.createLogger({ + level: level(), + levels, + transports, +}); + +module.exports = logger; diff --git a/api/jest.config.js b/api/jest.config.js index 17ca55fa2a..f441423fa0 100644 --- a/api/jest.config.js +++ b/api/jest.config.js @@ -3,7 +3,11 @@ module.exports = { clearMocks: true, roots: [''], coverageDirectory: 'coverage', - setupFiles: ['./test/jestSetup.js', './test/__mocks__/KeyvMongo.js'], + setupFiles: [ + './test/jestSetup.js', + './test/__mocks__/KeyvMongo.js', + './test/__mocks__/logger.js', + ], moduleNameMapper: { '~/(.*)': '/$1', }, diff --git a/api/lib/db/indexSync.js b/api/lib/db/indexSync.js index d753635499..5413d7b632 100644 --- a/api/lib/db/indexSync.js +++ b/api/lib/db/indexSync.js @@ -1,8 +1,10 @@ -const Conversation = require('../../models/schema/convoSchema'); -const Message = require('../../models/schema/messageSchema'); const { MeiliSearch } = require('meilisearch'); -let currentTimeout = null; +const Message = require('~/models/schema/messageSchema'); +const Conversation = require('~/models/schema/convoSchema'); +const { logger } = require('~/config'); + const searchEnabled = process.env?.SEARCH?.toLowerCase() === 'true'; +let currentTimeout = null; // eslint-disable-next-line no-unused-vars async function indexSync(req, res, next) { @@ -21,7 +23,7 @@ async function indexSync(req, res, next) { }); const { status } = await client.health(); - // console.log(`Meilisearch: ${status}`); + // logger.debug(`[indexSync] Meilisearch: ${status}`); const result = status === 'available' && !!process.env.SEARCH; if (!result) { @@ -35,39 +37,43 @@ async function indexSync(req, res, next) { const messagesIndexed = messages.numberOfDocuments; const convosIndexed = convos.numberOfDocuments; - console.log(`There are ${messageCount} messages in the database, ${messagesIndexed} indexed`); - console.log(`There are ${convoCount} convos in the database, ${convosIndexed} indexed`); + logger.debug( + `[indexSync] There are ${messageCount} messages in the database, ${messagesIndexed} indexed`, + ); + logger.debug( + `[indexSync] There are ${convoCount} convos in the database, ${convosIndexed} indexed`, + ); if (messageCount !== messagesIndexed) { - console.log('Messages out of sync, indexing'); + logger.debug('[indexSync] Messages out of sync, indexing'); Message.syncWithMeili(); } if (convoCount !== convosIndexed) { - console.log('Convos out of sync, indexing'); + logger.debug('[indexSync] Convos out of sync, indexing'); Conversation.syncWithMeili(); } } catch (err) { - // console.log('in index sync'); + // logger.debug('[indexSync] in index sync'); if (err.message.includes('not found')) { - console.log('Creating indices...'); + logger.debug('[indexSync] Creating indices...'); currentTimeout = setTimeout(async () => { try { await Message.syncWithMeili(); await Conversation.syncWithMeili(); } catch (err) { - console.error('Trouble creating indices, try restarting the server.'); + logger.error('[indexSync] Trouble creating indices, try restarting the server.', err); } }, 750); } else { - console.error(err); + logger.error('[indexSync] error', err); // res.status(500).json({ error: 'Server error' }); } } } process.on('exit', () => { - console.log('Clearing sync timeouts before exiting...'); + logger.debug('[indexSync] Clearing sync timeouts before exiting...'); clearTimeout(currentTimeout); }); diff --git a/api/models/Balance.js b/api/models/Balance.js index f0e6d73d1e..45dec69630 100644 --- a/api/models/Balance.js +++ b/api/models/Balance.js @@ -1,6 +1,7 @@ const mongoose = require('mongoose'); const balanceSchema = require('./schema/balance'); const { getMultiplier } = require('./tx'); +const { logger } = require('~/config'); balanceSchema.statics.check = async function ({ user, @@ -9,25 +10,21 @@ balanceSchema.statics.check = async function ({ valueKey, tokenType, amount, - debug, }) { const multiplier = getMultiplier({ valueKey, tokenType, model, endpoint }); const tokenCost = amount * multiplier; const { tokenCredits: balance } = (await this.findOne({ user }, 'tokenCredits').lean()) ?? {}; - if (debug) { - console.log('balance check', { - user, - model, - endpoint, - valueKey, - tokenType, - amount, - debug, - balance, - multiplier, - }); - } + logger.debug('[Balance.check]', { + user, + model, + endpoint, + valueKey, + tokenType, + amount, + balance, + multiplier, + }); if (!balance) { return { @@ -37,9 +34,7 @@ balanceSchema.statics.check = async function ({ }; } - if (debug) { - console.log('balance check', { tokenCost }); - } + logger.debug('[Balance.check]', { tokenCost }); return { canSpend: balance >= tokenCost, balance, tokenCost }; }; diff --git a/api/models/Config.js b/api/models/Config.js index d9de939146..fefb84b8f9 100644 --- a/api/models/Config.js +++ b/api/models/Config.js @@ -1,4 +1,6 @@ const mongoose = require('mongoose'); +const { logger } = require('~/config'); + const major = [0, 0]; const minor = [0, 0]; const patch = [0, 5]; @@ -69,7 +71,7 @@ module.exports = { try { return await Config.find(filter).lean(); } catch (error) { - console.error(error); + logger.error('Error getting configs', error); return { config: 'Error getting configs' }; } }, @@ -77,7 +79,7 @@ module.exports = { try { return await Config.deleteMany(filter); } catch (error) { - console.error(error); + logger.error('Error deleting configs', error); return { config: 'Error deleting configs' }; } }, diff --git a/api/models/Conversation.js b/api/models/Conversation.js index c946a28af6..f1aa7bfe71 100644 --- a/api/models/Conversation.js +++ b/api/models/Conversation.js @@ -1,12 +1,12 @@ -// const { Conversation } = require('./plugins'); const Conversation = require('./schema/convoSchema'); const { getMessages, deleteMessages } = require('./Message'); +const logger = require('~/config/winston'); const getConvo = async (user, conversationId) => { try { return await Conversation.findOne({ user, conversationId }).lean(); } catch (error) { - console.log(error); + logger.error('[getConvo] Error getting single conversation', error); return { message: 'Error getting single conversation' }; } }; @@ -26,7 +26,7 @@ module.exports = { upsert: true, }); } catch (error) { - console.log(error); + logger.error('[saveConvo] Error saving conversation', error); return { message: 'Error saving conversation' }; } }, @@ -41,7 +41,7 @@ module.exports = { .lean(); return { conversations: convos, pages: totalPages, pageNumber, pageSize }; } catch (error) { - console.log(error); + logger.error('[getConvosByPage] Error getting conversations', error); return { message: 'Error getting conversations' }; } }, @@ -87,7 +87,7 @@ module.exports = { convoMap, }; } catch (error) { - console.log(error); + logger.error('[getConvosQueried] Error getting conversations', error); return { message: 'Error fetching conversations' }; } }, @@ -104,7 +104,7 @@ module.exports = { return convo?.title || 'New Chat'; } } catch (error) { - console.log(error); + logger.error('[getConvoTitle] Error getting conversation title', error); return { message: 'Error getting conversation title' }; } }, @@ -123,7 +123,7 @@ module.exports = { * const user = 'someUserId'; * const filter = { someField: 'someValue' }; * const result = await deleteConvos(user, filter); - * console.log(result); // { n: 5, ok: 1, deletedCount: 5, messages: { n: 10, ok: 1, deletedCount: 10 } } + * logger.error(result); // { n: 5, ok: 1, deletedCount: 5, messages: { n: 10, ok: 1, deletedCount: 10 } } */ deleteConvos: async (user, filter) => { let toRemove = await Conversation.find({ ...filter, user }).select('conversationId'); diff --git a/api/models/Message.js b/api/models/Message.js index 1f9b8c16ab..270ff851f9 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -1,5 +1,6 @@ const { z } = require('zod'); const Message = require('./schema/messageSchema'); +const logger = require('~/config/winston'); const idSchema = z.string().uuid(); @@ -67,7 +68,7 @@ module.exports = { tokenCount, }; } catch (err) { - console.error(`Error saving message: ${err}`); + logger.error('Error saving message:', err); throw new Error('Failed to save message.'); } }, @@ -92,7 +93,7 @@ module.exports = { isEdited: true, }; } catch (err) { - console.error(`Error updating message: ${err}`); + logger.error('Error updating message:', err); throw new Error('Failed to update message.'); } }, @@ -106,7 +107,7 @@ module.exports = { }); } } catch (err) { - console.error(`Error deleting messages: ${err}`); + logger.error('Error deleting messages:', err); throw new Error('Failed to delete messages.'); } }, @@ -115,7 +116,7 @@ module.exports = { try { return await Message.find(filter).sort({ createdAt: 1 }).lean(); } catch (err) { - console.error(`Error getting messages: ${err}`); + logger.error('Error getting messages:', err); throw new Error('Failed to get messages.'); } }, @@ -124,7 +125,7 @@ module.exports = { try { return await Message.deleteMany(filter); } catch (err) { - console.error(`Error deleting messages: ${err}`); + logger.error('Error deleting messages:', err); throw new Error('Failed to delete messages.'); } }, diff --git a/api/models/Preset.js b/api/models/Preset.js index 553e2e7fce..e9f0a1e77e 100644 --- a/api/models/Preset.js +++ b/api/models/Preset.js @@ -1,10 +1,11 @@ const Preset = require('./schema/presetSchema'); +const { logger } = require('~/config'); const getPreset = async (user, presetId) => { try { return await Preset.findOne({ user, presetId }).lean(); } catch (error) { - console.log(error); + logger.error('[getPreset] Error getting single preset', error); return { message: 'Error getting single preset' }; } }; @@ -30,7 +31,7 @@ module.exports = { return presets; } catch (error) { - console.log(error); + logger.error('[getPresets] Error getting presets', error); return { message: 'Error retrieving presets' }; } }, @@ -62,7 +63,7 @@ module.exports = { setter.$set = update; return await Preset.findOneAndUpdate({ presetId, user }, setter, { new: true, upsert: true }); } catch (error) { - console.log(error); + logger.error('[savePreset] Error saving preset', error); return { message: 'Error saving preset' }; } }, diff --git a/api/models/Prompt.js b/api/models/Prompt.js index cd77b42b35..f2759472b6 100644 --- a/api/models/Prompt.js +++ b/api/models/Prompt.js @@ -1,4 +1,5 @@ const mongoose = require('mongoose'); +const { logger } = require('~/config'); const promptSchema = mongoose.Schema( { @@ -28,7 +29,7 @@ module.exports = { }); return { title, prompt }; } catch (error) { - console.error(error); + logger.error('Error saving prompt', error); return { prompt: 'Error saving prompt' }; } }, @@ -36,7 +37,7 @@ module.exports = { try { return await Prompt.find(filter).lean(); } catch (error) { - console.error(error); + logger.error('Error getting prompts', error); return { prompt: 'Error getting prompts' }; } }, @@ -44,7 +45,7 @@ module.exports = { try { return await Prompt.deleteMany(filter); } catch (error) { - console.error(error); + logger.error('Error deleting prompts', error); return { prompt: 'Error deleting prompts' }; } }, diff --git a/api/models/Session.js b/api/models/Session.js index d93ac526c1..de7e07400a 100644 --- a/api/models/Session.js +++ b/api/models/Session.js @@ -1,6 +1,8 @@ -const mongoose = require('mongoose'); const crypto = require('crypto'); -const signPayload = require('../server/services/signPayload'); +const mongoose = require('mongoose'); +const signPayload = require('~/server/services/signPayload'); +const { logger } = require('~/config'); + const { REFRESH_TOKEN_EXPIRY } = process.env ?? {}; const expires = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7; @@ -44,8 +46,8 @@ sessionSchema.methods.generateRefreshToken = async function () { return refreshToken; } catch (error) { - console.error( - 'Error generating refresh token. Have you set a JWT_REFRESH_SECRET in the .env file?\n\n', + logger.error( + 'Error generating refresh token. Is a `JWT_REFRESH_SECRET` set in the .env file?\n\n', error, ); throw error; @@ -59,10 +61,12 @@ sessionSchema.statics.deleteAllUserSessions = async function (userId) { } const result = await this.deleteMany({ user: userId }); if (result && result?.deletedCount > 0) { - console.log(`Deleted ${result.deletedCount} sessions for user ${userId}.`); + logger.debug( + `[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userId}.`, + ); } } catch (error) { - console.log('Error in deleting user sessions:', error); + logger.error('[deleteAllUserSessions] Error in deleting user sessions:', error); throw error; } }; diff --git a/api/models/checkBalance.js b/api/models/checkBalance.js index d36b77afe6..c0bbd060bf 100644 --- a/api/models/checkBalance.js +++ b/api/models/checkBalance.js @@ -13,7 +13,6 @@ const { logViolation } = require('../cache'); * @param {string} params.txData.user - The user ID or identifier. * @param {('prompt' | 'completion')} params.txData.tokenType - The type of token. * @param {number} params.txData.amount - The amount of tokens. - * @param {boolean} params.txData.debug - Debug flag. * @param {string} params.txData.model - The model name or identifier. * @returns {Promise} Returns true if the user can spend the amount, otherwise denies the request. * @throws {Error} Throws an error if there's an issue with the balance check. diff --git a/api/models/plugins/mongoMeili.js b/api/models/plugins/mongoMeili.js index 4d97ed5d03..abba848614 100644 --- a/api/models/plugins/mongoMeili.js +++ b/api/models/plugins/mongoMeili.js @@ -1,7 +1,9 @@ +const _ = require('lodash'); const mongoose = require('mongoose'); const { MeiliSearch } = require('meilisearch'); -const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc'); -const _ = require('lodash'); +const { cleanUpPrimaryKeyValue } = require('~/lib/utils/misc'); +const logger = require('~/config/meiliLogger'); + const searchEnabled = process.env.SEARCH && process.env.SEARCH.toLowerCase() === 'true'; const meiliEnabled = process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY && searchEnabled; @@ -64,8 +66,7 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { offset += batchSize; } - console.log('indexMap', indexMap.size); - console.log('mongoMap', mongoMap.size); + logger.debug('[syncWithMeili]', { indexMap: indexMap.size, mongoMap: mongoMap.size }); const updateOps = []; @@ -80,7 +81,11 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { (doc.text && doc.text !== mongoMap.get(id).text) || (doc.title && doc.title !== mongoMap.get(id).title) ) { - console.log(`${id} had document discrepancy in ${doc.text ? 'text' : 'title'} field`); + logger.debug( + `[syncWithMeili] ${id} had document discrepancy in ${ + doc.text ? 'text' : 'title' + } field`, + ); updateOps.push({ updateOne: { filter: update, update: { $set: { _meiliIndex: true } } }, }); @@ -116,15 +121,14 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { if (updateOps.length > 0) { await this.collection.bulkWrite(updateOps); - console.log( - `[Meilisearch] Finished indexing ${ + logger.debug( + `[syncWithMeili] Finished indexing ${ primaryKey === 'messageId' ? 'messages' : 'conversations' }`, ); } } catch (error) { - console.log('[Meilisearch] Error adding document to Meili'); - console.error(error); + logger.error('[syncWithMeili] Error adding document to Meili', error); } } @@ -143,7 +147,7 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { const query = {}; // query[primaryKey] = { $in: _.map(data.hits, primaryKey) }; query[primaryKey] = _.map(data.hits, (hit) => cleanUpPrimaryKeyValue(hit[primaryKey])); - // console.log('query', query); + // logger.debug('query', query); const hitsFromMongoose = await this.find( query, _.reduce( @@ -186,11 +190,11 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { async addObjectToMeili() { const object = this.preprocessObjectForIndex(); try { - // console.log('Adding document to Meili', object); + // logger.debug('Adding document to Meili', object); await index.addDocuments([object]); } catch (error) { - // console.log('Error adding document to Meili'); - // console.error(error); + // logger.debug('Error adding document to Meili'); + // logger.error(error); } await this.collection.updateMany({ _id: this._id }, { $set: { _meiliIndex: true } }); @@ -311,10 +315,10 @@ 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', + logger.error( + '[MeiliMongooseModel.deleteMany] There was an issue deleting conversation indexes upon deletion, next startup may be slow due to syncing', + error, ); - console.error(error); } return next(); } @@ -335,7 +339,11 @@ module.exports = function mongoMeili(schema, options) { try { meiliDoc = await client.index('convos').getDocument(doc.conversationId); } catch (error) { - console.log('[Meilisearch] Convo not found and will index', doc.conversationId); + logger.error( + '[MeiliMongooseModel.findOneAndUpdate] Convo not found in MeiliSearch and will index ' + + doc.conversationId, + error, + ); } } diff --git a/api/models/schema/messageSchema.js b/api/models/schema/messageSchema.js index 26648ab4ee..4c0ff2521e 100644 --- a/api/models/schema/messageSchema.js +++ b/api/models/schema/messageSchema.js @@ -1,5 +1,5 @@ const mongoose = require('mongoose'); -const mongoMeili = require('../plugins/mongoMeili'); +const mongoMeili = require('~/models/plugins/mongoMeili'); const messageSchema = mongoose.Schema( { messageId: { diff --git a/api/models/spendTokens.js b/api/models/spendTokens.js index abaab6145e..fe3a2be87a 100644 --- a/api/models/spendTokens.js +++ b/api/models/spendTokens.js @@ -1,4 +1,5 @@ const Transaction = require('./Transaction'); +const { logger } = require('~/config'); /** * Creates up to two transactions to record the spending of tokens. @@ -30,7 +31,7 @@ const spendTokens = async (txData, tokenUsage) => { } if (!completionTokens) { - this.debug && console.dir({ prompt, completion }, { depth: null }); + logger.debug('[spendTokens] !completionTokens', { prompt, completion }); return; } @@ -40,9 +41,9 @@ const spendTokens = async (txData, tokenUsage) => { rawAmount: -completionTokens, }); - this.debug && console.dir({ prompt, completion }, { depth: null }); + logger.debug('[spendTokens] post-transaction', { prompt, completion }); } catch (err) { - console.error(err); + logger.error('[spendTokens]', err); } }; diff --git a/api/package.json b/api/package.json index 354a35c727..478d812966 100644 --- a/api/package.json +++ b/api/package.json @@ -51,6 +51,7 @@ "jsonwebtoken": "^9.0.0", "keyv": "^4.5.4", "keyv-file": "^0.2.0", + "klona": "^2.0.6", "langchain": "^0.0.186", "librechat-data-provider": "*", "lodash": "^4.17.21", @@ -74,8 +75,10 @@ "pino": "^8.12.1", "sharp": "^0.32.6", "tiktoken": "^1.0.10", + "traverse": "^0.6.7", "ua-parser-js": "^1.0.36", - "winston": "^3.10.0", + "winston": "^3.11.0", + "winston-daily-rotate-file": "^4.7.1", "zod": "^3.22.4" }, "devDependencies": { diff --git a/api/server/controllers/AskController.js b/api/server/controllers/AskController.js index 8a30f0fad1..7ef25b93d1 100644 --- a/api/server/controllers/AskController.js +++ b/api/server/controllers/AskController.js @@ -1,7 +1,8 @@ +const { getResponseSender } = require('librechat-data-provider'); const { sendMessage, createOnProgress } = require('~/server/utils'); const { saveMessage, getConvoTitle, getConvo } = require('~/models'); -const { getResponseSender } = require('librechat-data-provider'); const { createAbortController, handleAbortError } = require('~/server/middleware'); +const { logger } = require('~/config'); const AskController = async (req, res, next, initializeClient) => { let { @@ -11,8 +12,7 @@ const AskController = async (req, res, next, initializeClient) => { parentMessageId = null, overrideParentMessageId = null, } = req.body; - console.log('ask log'); - console.dir({ text, conversationId, endpointOption }, { depth: null }); + logger.debug('[AskController]', { text, conversationId, ...endpointOption }); let metadata; let userMessage; let promptTokens; diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js index eeec0ef948..921ba3d838 100644 --- a/api/server/controllers/AuthController.js +++ b/api/server/controllers/AuthController.js @@ -1,13 +1,14 @@ const crypto = require('crypto'); const cookies = require('cookie'); const jwt = require('jsonwebtoken'); -const { Session, User } = require('../../models'); +const { Session, User } = require('~/models'); const { registerUser, - requestPasswordReset, resetPassword, setAuthTokens, -} = require('../services/AuthService'); + requestPasswordReset, +} = require('~/server/services/AuthService'); +const { logger } = require('~/config'); const registrationController = async (req, res) => { try { @@ -27,7 +28,7 @@ const registrationController = async (req, res) => { res.status(status).send({ message }); } } catch (err) { - console.log(err); + logger.error('[registrationController]', err); return res.status(500).json({ message: err.message }); } }; @@ -45,7 +46,7 @@ const resetPasswordRequestController = async (req, res) => { return res.status(200).json(resetService); } } catch (e) { - console.log(e); + logger.error('[resetPasswordRequestController]', e); return res.status(400).json({ message: e.message }); } }; @@ -63,7 +64,7 @@ const resetPasswordController = async (req, res) => { return res.status(200).json(resetPasswordService); } } catch (e) { - console.log(e); + logger.error('[resetPasswordController]', e); return res.status(400).json({ message: e.message }); } }; @@ -108,8 +109,7 @@ const refreshController = async (req, res) => { res.status(401).send('Refresh token expired or not found for this user'); } } catch (err) { - console.error('Refresh token error', refreshToken); - console.error(err); + logger.error(`[refreshController] Refresh token: ${refreshToken}`, err); res.status(403).send('Invalid refresh token'); } }; diff --git a/api/server/controllers/EditController.js b/api/server/controllers/EditController.js index 862b7ee3ba..8537d78098 100644 --- a/api/server/controllers/EditController.js +++ b/api/server/controllers/EditController.js @@ -1,7 +1,8 @@ +const { getResponseSender } = require('librechat-data-provider'); const { sendMessage, createOnProgress } = require('~/server/utils'); const { saveMessage, getConvoTitle, getConvo } = require('~/models'); -const { getResponseSender } = require('librechat-data-provider'); const { createAbortController, handleAbortError } = require('~/server/middleware'); +const { logger } = require('~/config'); const EditController = async (req, res, next, initializeClient) => { let { @@ -14,8 +15,13 @@ const EditController = async (req, res, next, initializeClient) => { parentMessageId = null, overrideParentMessageId = null, } = req.body; - console.log('edit log'); - console.dir({ text, generation, isContinued, conversationId, endpointOption }, { depth: null }); + logger.debug('[EditController]', { + text, + generation, + isContinued, + conversationId, + ...endpointOption, + }); let metadata; let userMessage; let promptTokens; diff --git a/api/server/controllers/ErrorController.js b/api/server/controllers/ErrorController.js index cdfd5b97a6..1308527b8c 100644 --- a/api/server/controllers/ErrorController.js +++ b/api/server/controllers/ErrorController.js @@ -1,15 +1,17 @@ +const { logger } = require('~/config'); + //handle duplicates const handleDuplicateKeyError = (err, res) => { + logger.error('Duplicate key error:', err.keyValue); const field = Object.keys(err.keyValue); const code = 409; const error = `An document with that ${field} already exists.`; - console.log('congrats you hit the duped keys error'); res.status(code).send({ messages: error, fields: field }); }; //handle validation errors const handleValidationError = (err, res) => { - console.log('congrats you hit the validation middleware'); + logger.error('Validation error:', err.errors); let errors = Object.values(err.errors).map((el) => el.message); let fields = Object.values(err.errors).map((el) => el.path); let code = 400; @@ -24,7 +26,6 @@ const handleValidationError = (err, res) => { // eslint-disable-next-line no-unused-vars module.exports = (err, req, res, next) => { try { - console.log('congrats you hit the error middleware'); if (err.name === 'ValidationError') { return (err = handleValidationError(err, res)); } @@ -32,6 +33,7 @@ module.exports = (err, req, res, next) => { return (err = handleDuplicateKeyError(err, res)); } } catch (err) { + logger.error('ErrorController => error', err); res.status(500).send('An unknown error occurred.'); } }; diff --git a/api/server/controllers/PluginController.js b/api/server/controllers/PluginController.js index 304c089657..697a499796 100644 --- a/api/server/controllers/PluginController.js +++ b/api/server/controllers/PluginController.js @@ -1,6 +1,8 @@ -const { promises: fs } = require('fs'); const path = require('path'); -const { addOpenAPISpecs } = require('../../app/clients/tools/util/addOpenAPISpecs'); +const { promises: fs } = require('fs'); +const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs'); +const { CacheKeys } = require('~/common/enums'); +const { getLogStores } = require('~/cache'); const filterUniquePlugins = (plugins) => { const seen = new Set(); @@ -27,6 +29,13 @@ const isPluginAuthenticated = (plugin) => { const getAvailablePluginsController = async (req, res) => { try { + const cache = getLogStores(CacheKeys.CONFIG); + const cachedPlugins = await cache.get(CacheKeys.PLUGINS); + if (cachedPlugins) { + res.status(200).json(cachedPlugins); + return; + } + const manifestFile = await fs.readFile( path.join(__dirname, '..', '..', 'app', 'clients', 'tools', 'manifest.json'), 'utf8', @@ -42,6 +51,7 @@ const getAvailablePluginsController = async (req, res) => { } }); const plugins = await addOpenAPISpecs(authenticatedPlugins); + await cache.set(CacheKeys.PLUGINS, plugins); res.status(200).json(plugins); } catch (error) { res.status(500).json({ message: error.message }); diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 21f03f686c..fa08cd5452 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -1,5 +1,6 @@ -const { updateUserPluginsService } = require('../services/UserService'); -const { updateUserPluginAuth, deleteUserPluginAuth } = require('../services/PluginService'); +const { updateUserPluginsService } = require('~/server/services/UserService'); +const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService'); +const { logger } = require('~/config'); const getUserController = async (req, res) => { res.status(200).send(req.user); @@ -13,7 +14,7 @@ const updateUserPluginsController = async (req, res) => { const userPluginsService = await updateUserPluginsService(user, pluginKey, action); if (userPluginsService instanceof Error) { - console.log(userPluginsService); + logger.error('[userPluginsService]', userPluginsService); const { status, message } = userPluginsService; res.status(status).send({ message }); } @@ -24,7 +25,7 @@ const updateUserPluginsController = async (req, res) => { for (let i = 0; i < keys.length; i++) { authService = await updateUserPluginAuth(user.id, keys[i], pluginKey, values[i]); if (authService instanceof Error) { - console.log(authService); + logger.error('[authService]', authService); const { status, message } = authService; res.status(status).send({ message }); } @@ -34,7 +35,7 @@ const updateUserPluginsController = async (req, res) => { for (let i = 0; i < keys.length; i++) { authService = await deleteUserPluginAuth(user.id, keys[i]); if (authService instanceof Error) { - console.log(authService); + logger.error('[authService]', authService); const { status, message } = authService; res.status(status).send({ message }); } @@ -44,7 +45,7 @@ const updateUserPluginsController = async (req, res) => { res.status(200).send(); } catch (err) { - console.log(err); + logger.error('[updateUserPluginsController]', err); res.status(500).json({ message: err.message }); } }; diff --git a/api/server/controllers/auth/LoginController.js b/api/server/controllers/auth/LoginController.js index 9c3b556f68..1b3b6180b9 100644 --- a/api/server/controllers/auth/LoginController.js +++ b/api/server/controllers/auth/LoginController.js @@ -1,5 +1,6 @@ -const User = require('../../../models/User'); -const { setAuthTokens } = require('../../services/AuthService'); +const User = require('~/models/User'); +const { setAuthTokens } = require('~/server/services/AuthService'); +const { logger } = require('~/config'); const loginController = async (req, res) => { try { @@ -15,7 +16,7 @@ const loginController = async (req, res) => { return res.status(200).send({ token, user }); } catch (err) { - console.log(err); + logger.error('[loginController]', err); } // Generic error messages are safer diff --git a/api/server/controllers/auth/LogoutController.js b/api/server/controllers/auth/LogoutController.js index 714a6466da..b09b8722aa 100644 --- a/api/server/controllers/auth/LogoutController.js +++ b/api/server/controllers/auth/LogoutController.js @@ -1,5 +1,6 @@ -const { logoutUser } = require('../../services/AuthService'); const cookies = require('cookie'); +const { logoutUser } = require('~/server/services/AuthService'); +const { logger } = require('~/config'); const logoutController = async (req, res) => { const refreshToken = req.headers.cookie ? cookies.parse(req.headers.cookie).refreshToken : null; @@ -9,7 +10,7 @@ const logoutController = async (req, res) => { res.clearCookie('refreshToken'); return res.status(status).send({ message }); } catch (err) { - console.log(err); + logger.error('[logoutController]', err); return res.status(500).json({ message: err.message }); } }; diff --git a/api/server/index.js b/api/server/index.js index 1120dfe6dc..afe9d1047a 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -6,8 +6,10 @@ const passport = require('passport'); const mongoSanitize = require('express-mongo-sanitize'); const errorController = require('./controllers/ErrorController'); const configureSocialLogins = require('./socialLogins'); -const { connectDb, indexSync } = require('../lib/db'); -const config = require('../config'); +const { connectDb, indexSync } = require('~/lib/db'); +const { logger } = require('~/config'); + +const paths = require('~/config/paths'); const routes = require('./routes'); const { PORT, HOST, ALLOW_SOCIAL_LOGIN } = process.env ?? {}; @@ -15,15 +17,15 @@ const { PORT, HOST, ALLOW_SOCIAL_LOGIN } = process.env ?? {}; const port = Number(PORT) || 3080; const host = HOST || 'localhost'; const projectPath = path.join(__dirname, '..', '..', 'client'); -const { jwtLogin, passportLogin } = require('../strategies'); +const { jwtLogin, passportLogin } = require('~/strategies'); const startServer = async () => { await connectDb(); - console.log('Connected to MongoDB'); + logger.info('Connected to MongoDB'); await indexSync(); const app = express(); - app.locals.config = config; + app.locals.config = paths; // Middleware app.use(errorController); @@ -77,11 +79,11 @@ const startServer = async () => { app.listen(port, host, () => { if (host == '0.0.0.0') { - console.log( + logger.info( `Server listening on all interfaces at port ${port}. Use http://localhost:${port} to access it`, ); } else { - console.log(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`); + logger.info(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`); } }); }; @@ -91,13 +93,12 @@ startServer(); let messageCount = 0; process.on('uncaughtException', (err) => { if (!err.message.includes('fetch failed')) { - console.error('There was an uncaught error:'); - console.error(err); + logger.error('There was an uncaught error:', err); } if (err.message.includes('fetch failed')) { if (messageCount === 0) { - console.error('Meilisearch error, search will be disabled'); + logger.warn('Meilisearch error, search will be disabled'); messageCount++; } @@ -105,7 +106,7 @@ process.on('uncaughtException', (err) => { } if (err.message.includes('OpenAIError') || err.message.includes('ChatCompletionMessage')) { - console.error( + logger.error( '\n\nAn Uncaught `OpenAIError` error may be due to your reverse-proxy setup or stream configuration, or a bug in the `openai` node package.', ); return; diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js index ea331bcbbd..9bf3b54e31 100644 --- a/api/server/middleware/abortMiddleware.js +++ b/api/server/middleware/abortMiddleware.js @@ -3,6 +3,7 @@ const { saveMessage, getConvo, getConvoTitle } = require('~/models'); const clearPendingReq = require('~/cache/clearPendingReq'); const abortControllers = require('./abortControllers'); const spendTokens = require('~/models/spendTokens'); +const { logger } = require('~/config'); async function abortMessage(req, res) { const { abortKey } = req.body; @@ -13,7 +14,7 @@ async function abortMessage(req, res) { const { abortController } = abortControllers.get(abortKey); const ret = await abortController.abortCompletion(); - console.log('Aborted request', abortKey); + logger.debug('[abortMessage] Aborted request', { abortKey }); abortControllers.delete(abortKey); res.send(JSON.stringify(ret)); } @@ -26,7 +27,7 @@ const handleAbort = () => { } return await abortMessage(req, res); } catch (err) { - console.error(err); + logger.error('[abortMessage] handleAbort error', err); } }; }; @@ -82,7 +83,7 @@ const createAbortController = (req, res, getAbortData) => { }; const handleAbortError = async (res, req, error, data) => { - console.error(error); + logger.error('[handleAbortError] response error and aborting request', error); const { sender, conversationId, messageId, parentMessageId, partialText } = data; const respondWithError = async () => { @@ -110,7 +111,7 @@ const handleAbortError = async (res, req, error, data) => { try { return await abortMessage(req, res); } catch (err) { - console.error(err); + logger.error('[handleAbortError] error while trying to abort message', err); return respondWithError(); } } else { diff --git a/api/server/routes/ask/addToCache.js b/api/server/routes/ask/addToCache.js index 616c9d91b0..4ecdea0e0c 100644 --- a/api/server/routes/ask/addToCache.js +++ b/api/server/routes/ask/addToCache.js @@ -1,5 +1,6 @@ const Keyv = require('keyv'); const { KeyvFile } = require('keyv-file'); +const { logger } = require('~/config'); const addToCache = async ({ endpoint, endpointOption, userMessage, responseMessage }) => { try { @@ -57,7 +58,7 @@ const addToCache = async ({ endpoint, endpointOption, userMessage, responseMessa await conversationsCache.set(conversationId, conversation); } catch (error) { - console.error('Trouble adding to cache', error); + logger.error('[addToCache] Error adding conversation to cache', error); } }; diff --git a/api/server/routes/ask/askChatGPTBrowser.js b/api/server/routes/ask/askChatGPTBrowser.js index 04772a74a3..37065b3770 100644 --- a/api/server/routes/ask/askChatGPTBrowser.js +++ b/api/server/routes/ask/askChatGPTBrowser.js @@ -1,10 +1,12 @@ -const express = require('express'); const crypto = require('crypto'); +const express = require('express'); +const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('~/models'); +const { handleError, sendMessage, createOnProgress, handleText } = require('~/server/utils'); +const { setHeaders } = require('~/server/middleware'); +const { browserClient } = require('~/app/'); +const { logger } = require('~/config'); + const router = express.Router(); -const { browserClient } = require('../../../app/'); -const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models'); -const { handleError, sendMessage, createOnProgress, handleText } = require('../../utils'); -const { setHeaders } = require('../../middleware'); router.post('/', setHeaders, async (req, res) => { const { @@ -41,10 +43,10 @@ router.post('/', setHeaders, async (req, res) => { key: req.body?.key ?? null, }; - console.log('ask log', { + logger.debug('[/ask/chatGPTBrowser]', { userMessage, - endpointOption, conversationId, + ...endpointOption, }); if (!overrideParentMessageId) { @@ -136,7 +138,7 @@ const ask = async ({ }, }); - console.log('CLIENT RESPONSE', response); + logger.debug('[/ask/chatGPTBrowser]', response); const newConversationId = response.conversationId || conversationId; const newUserMassageId = response.parentMessageId || userMessageId; diff --git a/api/server/routes/ask/bingAI.js b/api/server/routes/ask/bingAI.js index 4a170209b6..7a7177a96e 100644 --- a/api/server/routes/ask/bingAI.js +++ b/api/server/routes/ask/bingAI.js @@ -1,10 +1,12 @@ const express = require('express'); const crypto = require('crypto'); +const { handleError, sendMessage, createOnProgress, handleText } = require('~/server/utils'); +const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('~/models'); +const { setHeaders } = require('~/server/middleware'); +const { titleConvoBing, askBing } = require('~/app'); +const { logger } = require('~/config'); + const router = express.Router(); -const { titleConvoBing, askBing } = require('../../../app'); -const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models'); -const { handleError, sendMessage, createOnProgress, handleText } = require('../../utils'); -const { setHeaders } = require('../../middleware'); router.post('/', setHeaders, async (req, res) => { const { @@ -60,7 +62,7 @@ router.post('/', setHeaders, async (req, res) => { }; } - console.log('ask log', { + logger.debug('[/ask/bingAI] ask log', { userMessage, endpointOption, conversationId, @@ -153,10 +155,10 @@ const ask = async ({ abortController, }); - console.log('BING RESPONSE', response); + logger.debug('[/ask/bingAI] BING RESPONSE', response); if (response.details && response.details.scores) { - console.log('SCORES', response.details.scores); + logger.debug('[/ask/bingAI] SCORES', response.details.scores); } const newConversationId = endpointOption?.jailbreak @@ -250,7 +252,7 @@ const ask = async ({ }); } } catch (error) { - console.error(error); + logger.error('[/ask/bingAI] Error handling BingAI response', error); const partialText = getPartialText(); if (partialText?.length > 2) { const responseMessage = { @@ -276,7 +278,7 @@ const ask = async ({ responseMessage: responseMessage, }; } else { - console.log(error); + logger.error('[/ask/bingAI] Error handling BingAI response', error); const errorMessage = { messageId: responseMessageId, sender: model, diff --git a/api/server/routes/ask/gptPlugins.js b/api/server/routes/ask/gptPlugins.js index f93f9e95f0..b0aa1aa0f0 100644 --- a/api/server/routes/ask/gptPlugins.js +++ b/api/server/routes/ask/gptPlugins.js @@ -14,6 +14,7 @@ const { validateEndpoint, buildEndpointOption, } = require('~/server/middleware'); +const { logger } = require('~/config'); router.post('/abort', handleAbort()); @@ -25,8 +26,7 @@ router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, parentMessageId = null, overrideParentMessageId = null, } = req.body; - console.log('ask log'); - console.dir({ text, conversationId, endpointOption }, { depth: null }); + logger.debug('[/ask/gptPlugins]', { text, conversationId, ...endpointOption }); let metadata; let userMessage; let promptTokens; @@ -189,8 +189,8 @@ router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, response = { ...response, ...metadata }; } - console.log('CLIENT RESPONSE'); - console.dir(response, { depth: null }); + logger.debug('[/ask/gptPlugins]', response); + response.plugins = plugins.map((p) => ({ ...p, loading: false })); await saveMessage({ ...response, user }); diff --git a/api/server/routes/ask/openAI.js b/api/server/routes/ask/openAI.js index 1f292bee8f..e91692d8a8 100644 --- a/api/server/routes/ask/openAI.js +++ b/api/server/routes/ask/openAI.js @@ -12,6 +12,7 @@ const { validateEndpoint, buildEndpointOption, } = require('~/server/middleware'); +const { logger } = require('~/config'); router.post('/abort', handleAbort()); @@ -23,8 +24,9 @@ router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, parentMessageId = null, overrideParentMessageId = null, } = req.body; - console.log('ask log'); - console.dir({ text, conversationId, endpointOption }, { depth: null }); + + logger.debug('[/ask/openAI]', { text, conversationId, ...endpointOption }); + let metadata; let userMessage; let promptTokens; diff --git a/api/server/routes/assistants/assistants.js b/api/server/routes/assistants/assistants.js index a33729b2b6..b911c685aa 100644 --- a/api/server/routes/assistants/assistants.js +++ b/api/server/routes/assistants/assistants.js @@ -1,5 +1,7 @@ const OpenAI = require('openai'); const express = require('express'); +const { logger } = require('~/config'); + const router = express.Router(); /** @@ -13,7 +15,7 @@ router.post('/', async (req, res) => { const openai = new OpenAI(process.env.OPENAI_API_KEY); const assistantData = req.body; const assistant = await openai.beta.assistants.create(assistantData); - console.log(assistant); + logger.debug('/assistants/', assistant); res.status(201).json(assistant); } catch (error) { res.status(500).json({ error: error.message }); diff --git a/api/server/routes/assistants/chat.js b/api/server/routes/assistants/chat.js index 71cbef2218..e45bad191e 100644 --- a/api/server/routes/assistants/chat.js +++ b/api/server/routes/assistants/chat.js @@ -1,5 +1,6 @@ const crypto = require('crypto'); const OpenAI = require('openai'); +const { logger } = require('~/config'); const { sendMessage } = require('../../utils'); const { initThread, createRun, handleRun } = require('../../services/AssistantService'); const express = require('express'); @@ -23,7 +24,7 @@ const { */ router.post('/', setHeaders, async (req, res) => { try { - console.log(req.body); + logger.debug('[/assistants/chat/] req.body', req.body); // test message: // How many polls of 500 ms intervals are there in 18 seconds? @@ -100,7 +101,7 @@ router.post('/', setHeaders, async (req, res) => { res.end(); } catch (error) { // res.status(500).json({ error: error.message }); - console.error(error); + logger.error('[/assistants/chat/]', error); res.end(); } }); diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 9c02aa53fe..85889f4b81 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -1,6 +1,8 @@ const express = require('express'); +const { isEnabled } = require('~/server/utils'); +const { logger } = require('~/config'); + const router = express.Router(); -const { isEnabled } = require('../utils'); const emailLoginEnabled = process.env.ALLOW_EMAIL_LOGIN === undefined || isEnabled(process.env.ALLOW_EMAIL_LOGIN); @@ -38,7 +40,7 @@ router.get('/', async function (req, res) { return res.status(200).send(payload); } catch (err) { - console.error(err); + logger.error('Error in startup config', err); return res.status(500).send({ error: err.message }); } }); diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index d4b919d309..4395df0fee 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -1,8 +1,9 @@ const express = require('express'); const router = express.Router(); -const { getConvo, saveConvo } = require('../../models'); -const { getConvosByPage, deleteConvos } = require('../../models/Conversation'); -const requireJwtAuth = require('../middleware/requireJwtAuth'); +const { getConvosByPage, deleteConvos } = require('~/models/Conversation'); +const requireJwtAuth = require('~/server/middleware/requireJwtAuth'); +const { getConvo, saveConvo } = require('~/models'); +const { logger } = require('~/config'); router.use(requireJwtAuth); @@ -30,7 +31,7 @@ router.post('/clear', async (req, res) => { } // for debugging deletion source - // console.log('source:', source); + // logger.debug('source:', source); if (source === 'button' && !conversationId) { return res.status(200).send('No conversationId provided'); @@ -40,7 +41,7 @@ router.post('/clear', async (req, res) => { const dbResponse = await deleteConvos(req.user.id, filter); res.status(201).send(dbResponse); } catch (error) { - console.error(error); + logger.error('Error clearing conversations', error); res.status(500).send(error); } }); @@ -52,7 +53,7 @@ router.post('/update', async (req, res) => { const dbResponse = await saveConvo(req.user.id, update); res.status(201).send(dbResponse); } catch (error) { - console.error(error); + logger.error('Error updating conversation', error); res.status(500).send(error); } }); diff --git a/api/server/routes/edit/gptPlugins.js b/api/server/routes/edit/gptPlugins.js index f396663503..b4f1f7ce85 100644 --- a/api/server/routes/edit/gptPlugins.js +++ b/api/server/routes/edit/gptPlugins.js @@ -1,8 +1,8 @@ const express = require('express'); const router = express.Router(); const { validateTools } = require('~/app'); -const { saveMessage, getConvoTitle, getConvo } = require('~/models'); const { getResponseSender } = require('librechat-data-provider'); +const { saveMessage, getConvoTitle, getConvo } = require('~/models'); const { initializeClient } = require('~/server/services/Endpoints/gptPlugins'); const { sendMessage, createOnProgress, formatSteps, formatAction } = require('~/server/utils'); const { @@ -13,6 +13,7 @@ const { validateEndpoint, buildEndpointOption, } = require('~/server/middleware'); +const { logger } = require('~/config'); router.post('/abort', handleAbort()); @@ -27,8 +28,14 @@ router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, parentMessageId = null, overrideParentMessageId = null, } = req.body; - console.log('edit log'); - console.dir({ text, generation, isContinued, conversationId, endpointOption }, { depth: null }); + + logger.debug('[/edit/gptPlugins]', { + text, + generation, + isContinued, + conversationId, + ...endpointOption, + }); let metadata; let userMessage; let promptTokens; @@ -102,7 +109,7 @@ router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, saveMessage({ ...userMessage, user }); } sendIntermediateMessage(res, { plugin }); - // console.log('PLUGIN ACTION', formattedAction); + // logger.debug('PLUGIN ACTION', formattedAction); }; const onChainEnd = (data) => { @@ -111,7 +118,7 @@ router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, plugin.loading = false; saveMessage({ ...userMessage, user }); sendIntermediateMessage(res, { plugin }); - // console.log('CHAIN END', plugin.outputs); + // logger.debug('CHAIN END', plugin.outputs); }; const getAbortData = () => ({ @@ -162,8 +169,7 @@ router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, response = { ...response, ...metadata }; } - console.log('CLIENT RESPONSE'); - console.dir(response, { depth: null }); + logger.debug('[/edit/gptPlugins] CLIENT RESPONSE', response); response.plugin = { ...plugin, loading: false }; await saveMessage({ ...response, user }); diff --git a/api/server/routes/edit/openAI.js b/api/server/routes/edit/openAI.js index 11e993903a..ec0b62330d 100644 --- a/api/server/routes/edit/openAI.js +++ b/api/server/routes/edit/openAI.js @@ -12,6 +12,7 @@ const { validateEndpoint, buildEndpointOption, } = require('~/server/middleware'); +const { logger } = require('~/config'); router.post('/abort', handleAbort()); @@ -26,8 +27,15 @@ router.post('/', validateEndpoint, buildEndpointOption, setHeaders, async (req, parentMessageId = null, overrideParentMessageId = null, } = req.body; - console.log('edit log'); - console.dir({ text, generation, isContinued, conversationId, endpointOption }, { depth: null }); + + logger.debug('[/edit/openAI]', { + text, + generation, + isContinued, + conversationId, + ...endpointOption, + }); + let metadata; let userMessage; let promptTokens; diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index b1f4622eb3..d9df1bdd75 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -1,8 +1,9 @@ const { z } = require('zod'); +const path = require('path'); const fs = require('fs').promises; const express = require('express'); const { deleteFiles } = require('~/models'); -const path = require('path'); +const { logger } = require('~/config'); const router = express.Router(); @@ -55,7 +56,7 @@ router.delete('/', async (req, res) => { await Promise.all(promises); res.status(200).json({ message: 'Files deleted successfully' }); } catch (error) { - console.error('Error deleting files:', error); + logger.error('[/files] Error deleting files:', error); res.status(400).json({ message: 'Error in request', error: error.message }); } }); diff --git a/api/server/routes/files/images.js b/api/server/routes/files/images.js index b8aaa6bc4d..f88b7f2c7a 100644 --- a/api/server/routes/files/images.js +++ b/api/server/routes/files/images.js @@ -3,6 +3,7 @@ const fs = require('fs').promises; const express = require('express'); const upload = require('./multer'); const { localStrategy } = require('~/server/services/Files'); +const { logger } = require('~/config'); const router = express.Router(); @@ -35,11 +36,11 @@ router.post('/', upload.single('file'), async (req, res) => { metadata.file_id = req.file_id; await localStrategy({ req, res, file, metadata }); } catch (error) { - console.error('Error processing file:', error); + logger.error('[/files/images] Error processing file:', error); try { await fs.unlink(file.path); } catch (error) { - console.error('Error deleting file:', error); + logger.error('[/files/images] Error deleting file:', error); } res.status(500).json({ message: 'Error processing file' }); } @@ -49,7 +50,7 @@ router.post('/', upload.single('file'), async (req, res) => { // try { // // await fs.unlink(file.path); // } catch (error) { - // console.error('Error deleting file:', error); + // logger.error('[/files/images] Error deleting file:', error); // } // } diff --git a/api/server/routes/oauth.js b/api/server/routes/oauth.js index e408539344..816fc7200f 100644 --- a/api/server/routes/oauth.js +++ b/api/server/routes/oauth.js @@ -1,8 +1,10 @@ const passport = require('passport'); const express = require('express'); const router = express.Router(); -const { setAuthTokens } = require('../services/AuthService'); -const { loginLimiter, checkBan } = require('../middleware'); +const { setAuthTokens } = require('~/server/services/AuthService'); +const { loginLimiter, checkBan } = require('~/server/middleware'); +const { logger } = require('~/config'); + const domains = { client: process.env.DOMAIN_CLIENT, server: process.env.DOMAIN_SERVER, @@ -19,7 +21,7 @@ const oauthHandler = async (req, res) => { await setAuthTokens(req.user._id, res); res.redirect(domains.client); } catch (err) { - console.error('Error in setting authentication tokens:', err); + logger.error('Error in setting authentication tokens:', err); } }; diff --git a/api/server/routes/presets.js b/api/server/routes/presets.js index e21d2df9d3..76aaed698c 100644 --- a/api/server/routes/presets.js +++ b/api/server/routes/presets.js @@ -1,8 +1,10 @@ const express = require('express'); -const router = express.Router(); -const { getPresets, savePreset, deletePresets } = require('../../models'); const crypto = require('crypto'); -const requireJwtAuth = require('../middleware/requireJwtAuth'); +const { getPresets, savePreset, deletePresets } = require('~/models'); +const requireJwtAuth = require('~/server/middleware/requireJwtAuth'); +const { logger } = require('~/config'); + +const router = express.Router(); router.get('/', requireJwtAuth, async (req, res) => { const presets = (await getPresets(req.user.id)).map((preset) => preset); @@ -18,7 +20,7 @@ router.post('/', requireJwtAuth, async (req, res) => { const preset = await savePreset(req.user.id, update); res.status(201).send(preset); } catch (error) { - console.error(error); + logger.error('[/presets] error saving preset', error); res.status(500).send(error); } }); @@ -31,13 +33,13 @@ router.post('/delete', requireJwtAuth, async (req, res) => { filter = { presetId }; } - console.log('delete preset filter', filter); + logger.debug('[/presets/delete] delete preset filter', filter); try { const deleteCount = await deletePresets(req.user.id, filter); res.status(201).send(deleteCount); } catch (error) { - console.error(error); + logger.error('[/presets/delete] error deleting presets', error); res.status(500).send(error); } }); diff --git a/api/server/routes/search.js b/api/server/routes/search.js index 98720a2ae5..2197b38ce4 100644 --- a/api/server/routes/search.js +++ b/api/server/routes/search.js @@ -1,14 +1,16 @@ const Keyv = require('keyv'); const express = require('express'); -const router = express.Router(); const { MeiliSearch } = require('meilisearch'); -const { Message } = require('../../models/Message'); -const { Conversation, getConvosQueried } = require('../../models/Conversation'); -const { reduceHits } = require('../../lib/utils/reduceHits'); -const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc'); -const requireJwtAuth = require('../middleware/requireJwtAuth'); -const keyvRedis = require('../../cache/keyvRedis'); -const { isEnabled } = require('../utils'); +const { Conversation, getConvosQueried } = require('~/models/Conversation'); +const requireJwtAuth = require('~/server/middleware/requireJwtAuth'); +const { cleanUpPrimaryKeyValue } = require('~/lib/utils/misc'); +const { reduceHits } = require('~/lib/utils/reduceHits'); +const { isEnabled } = require('~/server/utils'); +const { Message } = require('~/models/Message'); +const keyvRedis = require('~/cache/keyvRedis'); +const { logger } = require('~/config'); + +const router = express.Router(); const expiration = 60 * 1000; const cache = isEnabled(process.env.USE_REDIS) @@ -31,7 +33,7 @@ router.get('/', async function (req, res) { const key = `${user}:search:${q}`; const cached = await cache.get(key); if (cached) { - console.log('cache hit', key); + logger.debug('[/search] cache hit: ' + key); const { pages, pageSize, messages } = cached; res .status(200) @@ -39,7 +41,6 @@ router.get('/', async function (req, res) { return; } - // const message = await Message.meiliSearch(q); const messages = ( await Message.meiliSearch( q, @@ -61,8 +62,8 @@ router.get('/', async function (req, res) { const titles = (await Conversation.meiliSearch(q)).hits; const sortedHits = reduceHits(messages, titles); // debugging: - // console.log('user:', user, 'message hits:', messages.length, 'convo hits:', titles.length); - // console.log('sorted hits:', sortedHits.length); + // logger.debug('user:', user, 'message hits:', messages.length, 'convo hits:', titles.length); + // logger.debug('sorted hits:', sortedHits.length); const result = await getConvosQueried(user, sortedHits, pageNumber); const activeMessages = []; @@ -86,10 +87,10 @@ router.get('/', async function (req, res) { } delete result.convoMap; // for debugging - // console.log(result, messages.length); + // logger.debug(result, messages.length); res.status(200).send(result); } catch (error) { - console.log(error); + logger.error('[/search] Error while searching messages & conversations', error); res.status(500).send({ message: 'Error searching' }); } }); @@ -114,11 +115,9 @@ router.get('/enable', async function (req, res) { }); const { status } = await client.health(); - // console.log(`Meilisearch: ${status}`); result = status === 'available' && !!process.env.SEARCH; return res.send(result); } catch (error) { - // console.error(error); return res.send(false); } }); diff --git a/api/server/routes/tokenizer.js b/api/server/routes/tokenizer.js index fd66c20e62..581f82bf2a 100644 --- a/api/server/routes/tokenizer.js +++ b/api/server/routes/tokenizer.js @@ -1,7 +1,8 @@ const express = require('express'); const router = express.Router(); -const { countTokens } = require('../utils'); -const requireJwtAuth = require('../middleware/requireJwtAuth'); +const requireJwtAuth = require('~/server/middleware/requireJwtAuth'); +const { countTokens } = require('~/server/utils'); +const { logger } = require('~/config'); router.post('/', requireJwtAuth, async (req, res) => { try { @@ -9,7 +10,7 @@ router.post('/', requireJwtAuth, async (req, res) => { const count = await countTokens(arg?.text ?? arg); res.send({ count }); } catch (e) { - console.error(e); + logger.error('[/tokenizer] Error counting tokens', e); res.status(500).send(e.message); } }); diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index 363042a753..a60ae370ef 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -1,10 +1,12 @@ const crypto = require('crypto'); const bcrypt = require('bcryptjs'); -const User = require('../../models/User'); -const Session = require('../../models/Session'); -const Token = require('../../models/schema/tokenSchema'); -const { registerSchema, errorsToString } = require('../../strategies/validators'); -const { sendEmail } = require('../utils'); +const { registerSchema, errorsToString } = require('~/strategies/validators'); +const Token = require('~/models/schema/tokenSchema'); +const { sendEmail } = require('~/server/utils'); +const Session = require('~/models/Session'); +const { logger } = require('~/config'); +const User = require('~/models/User'); + const domains = { client: process.env.DOMAIN_CLIENT, server: process.env.DOMAIN_SERVER, @@ -29,7 +31,7 @@ const logoutUser = async (userId, refreshToken) => { try { await Session.deleteOne({ _id: session._id }); } catch (deleteErr) { - console.error(deleteErr); + logger.error('[logoutUser] Failed to delete session.', deleteErr); return { status: 500, message: 'Failed to delete session.' }; } } @@ -50,7 +52,7 @@ const registerUser = async (user) => { const { error } = registerSchema.safeParse(user); if (error) { const errorMessage = errorsToString(error.errors); - console.info( + logger.info( 'Route: register - Validation Error', { name: 'Request params:', value: user }, { name: 'Validation error:', value: errorMessage }, @@ -65,7 +67,7 @@ const registerUser = async (user) => { const existingUser = await User.findOne({ email }).lean(); if (existingUser) { - console.info( + logger.info( 'Register User - Email in use', { name: 'Request params:', value: user }, { name: 'Existing user:', value: existingUser }, @@ -229,7 +231,7 @@ const setAuthTokens = async (userId, res, sessionId = null) => { return token; } catch (error) { - console.log('Error in setting authentication tokens:', error); + logger.error('[setAuthTokens] Error in setting authentication tokens:', error); throw error; } }; diff --git a/api/server/services/Files/save.js b/api/server/services/Files/save.js index d598bf9b9f..08f6a0d5cc 100644 --- a/api/server/services/Files/save.js +++ b/api/server/services/Files/save.js @@ -1,5 +1,6 @@ const fs = require('fs'); const path = require('path'); +const { logger } = require('~/config'); /** * Saves a file to a specified output path with a new filename. @@ -24,7 +25,7 @@ async function saveFile(file, outputPath, outputFilename) { return outputFilePath; } catch (error) { - console.error('Error while saving the file:', error); + logger.error('[saveFile] Error while saving the file:', error); throw error; } } diff --git a/api/server/services/ModelService.js b/api/server/services/ModelService.js index 009e9c6593..d405fe3318 100644 --- a/api/server/services/ModelService.js +++ b/api/server/services/ModelService.js @@ -1,10 +1,13 @@ -const HttpsProxyAgent = require('https-proxy-agent'); -const axios = require('axios'); const Keyv = require('keyv'); +const axios = require('axios'); +const HttpsProxyAgent = require('https-proxy-agent'); const { isEnabled } = require('~/server/utils'); -const { extractBaseURL } = require('~/utils'); const keyvRedis = require('~/cache/keyvRedis'); +const { extractBaseURL } = require('~/utils'); +const { logger } = require('~/config'); + // const { getAzureCredentials, genAzureChatCompletion } = require('~/utils/'); + const { openAIApiKey, userProvidedOpenAI } = require('./Config/EndpointService').config; const modelsCache = isEnabled(process.env.USE_REDIS) @@ -54,9 +57,9 @@ const fetchOpenAIModels = async (opts = { azure: false, plugins: false }, _model const res = await axios.get(`${basePath}${opts.azure ? '' : '/models'}`, payload); models = res.data.data.map((item) => item.id); - // console.log(`Fetched ${models.length} models from ${opts.azure ? 'Azure ' : ''}OpenAI API`); + // logger.debug(`Fetched ${models.length} models from ${opts.azure ? 'Azure ' : ''}OpenAI API`); } catch (err) { - console.log(`Failed to fetch models from ${opts.azure ? 'Azure ' : ''}OpenAI API`); + logger.error(`Failed to fetch models from ${opts.azure ? 'Azure ' : ''}OpenAI API`, err); } } diff --git a/api/server/services/PluginService.js b/api/server/services/PluginService.js index 8e8643cbfe..1eaa6eedab 100644 --- a/api/server/services/PluginService.js +++ b/api/server/services/PluginService.js @@ -1,5 +1,6 @@ -const PluginAuth = require('../../models/schema/pluginAuthSchema'); -const { encrypt, decrypt } = require('../utils/'); +const PluginAuth = require('~/models/schema/pluginAuthSchema'); +const { encrypt, decrypt } = require('~/server/utils/'); +const { logger } = require('~/config'); const getUserPluginAuthValue = async (user, authField) => { try { @@ -11,7 +12,7 @@ const getUserPluginAuthValue = async (user, authField) => { const decryptedValue = decrypt(pluginAuth.value); return decryptedValue; } catch (err) { - console.log(err); + logger.error('[getUserPluginAuthValue]', err); return err; } }; @@ -36,7 +37,7 @@ const getUserPluginAuthValue = async (user, authField) => { // return pluginAuth; // } catch (err) { -// console.log(err); +// logger.error('[getUserPluginAuthValue]', err); // return err; // } // }; @@ -62,7 +63,7 @@ const updateUserPluginAuth = async (userId, authField, pluginKey, value) => { return newPluginAuth; } } catch (err) { - console.log(err); + logger.error('[updateUserPluginAuth]', err); return err; } }; @@ -72,7 +73,7 @@ const deleteUserPluginAuth = async (userId, authField) => { const response = await PluginAuth.deleteOne({ userId, authField }); return response; } catch (err) { - console.log(err); + logger.error('[deleteUserPluginAuth]', err); return err; } }; diff --git a/api/server/services/Runs/RunMananger.js b/api/server/services/Runs/RunMananger.js index adc0e1819d..67a3624c18 100644 --- a/api/server/services/Runs/RunMananger.js +++ b/api/server/services/Runs/RunMananger.js @@ -1,3 +1,5 @@ +const { logger } = require('~/config'); + /** * @typedef {import('openai').OpenAI} OpenAI * @typedef {import('../AssistantService').RunStep} RunStep @@ -84,8 +86,12 @@ class RunManager { return await this.handlers['final']({ step, runStatus, stepsByStatus: this.stepsByStatus }); } - console.log(`Default handler for ${step.id} with status \`${runStatus}\``); - console.dir({ step, runStatus, final, isLast }, { depth: null }); + logger.debug(`[RunManager] Default handler for ${step.id} with status \`${runStatus}\``, { + step, + runStatus, + final, + isLast, + }); return step; } } diff --git a/api/server/services/UserService.js b/api/server/services/UserService.js index c3a25f3b92..4a9d3abe7b 100644 --- a/api/server/services/UserService.js +++ b/api/server/services/UserService.js @@ -1,5 +1,6 @@ -const { User, Key } = require('../../models'); -const { encrypt, decrypt } = require('../utils'); +const { User, Key } = require('~/models'); +const { encrypt, decrypt } = require('~/server/utils'); +const { logger } = require('~/config'); const updateUserPluginsService = async (user, pluginKey, action) => { try { @@ -15,7 +16,7 @@ const updateUserPluginsService = async (user, pluginKey, action) => { ); } } catch (err) { - console.log(err); + logger.error('[updateUserPluginsService]', err); return err; } }; diff --git a/api/server/utils/countTokens.js b/api/server/utils/countTokens.js index cc40fdd7cf..9c8c98e76a 100644 --- a/api/server/utils/countTokens.js +++ b/api/server/utils/countTokens.js @@ -1,7 +1,8 @@ -const { Tiktoken } = require('tiktoken/lite'); const { load } = require('tiktoken/load'); +const { Tiktoken } = require('tiktoken/lite'); const registry = require('tiktoken/registry.json'); const models = require('tiktoken/model_to_encoding.json'); +const logger = require('~/config/winston'); const countTokens = async (text = '', modelName = 'gpt-3.5-turbo') => { let encoder = null; @@ -12,7 +13,7 @@ const countTokens = async (text = '', modelName = 'gpt-3.5-turbo') => { encoder.free(); return tokens.length; } catch (e) { - console.error(e); + logger.error('[countTokens]', e); if (encoder) { encoder.free(); } diff --git a/api/server/utils/crypto.js b/api/server/utils/crypto.js index efa89de4fc..9b5fed67c6 100644 --- a/api/server/utils/crypto.js +++ b/api/server/utils/crypto.js @@ -1,3 +1,5 @@ +require('dotenv').config(); + const crypto = require('crypto'); const key = Buffer.from(process.env.CREDS_KEY, 'hex'); const iv = Buffer.from(process.env.CREDS_IV, 'hex'); diff --git a/api/server/utils/handleText.js b/api/server/utils/handleText.js index 3ae18e98c5..4cd1b7ce99 100644 --- a/api/server/utils/handleText.js +++ b/api/server/utils/handleText.js @@ -1,6 +1,6 @@ const partialRight = require('lodash/partialRight'); -const { getCitations, citeText } = require('./citations'); const { sendMessage } = require('./streamResponse'); +const { getCitations, citeText } = require('./citations'); const cursor = ''; const citationRegex = /\[\^\d+?\^]/g; @@ -138,21 +138,31 @@ function formatAction(action) { } /** - * Checks if the given string value is truthy by comparing it to the string 'true' (case-insensitive). + * Checks if the given value is truthy by being either the boolean `true` or a string + * that case-insensitively matches 'true'. * * @function - * @param {string|null|undefined} value - The string value to check. - * @returns {boolean} Returns `true` if the value is a case-insensitive match for the string 'true', otherwise returns `false`. + * @param {string|boolean|null|undefined} value - The value to check. + * @returns {boolean} Returns `true` if the value is the boolean `true` or a case-insensitive + * match for the string 'true', otherwise returns `false`. * @example * * isEnabled("True"); // returns true * isEnabled("TRUE"); // returns true + * isEnabled(true); // returns true * isEnabled("false"); // returns false + * isEnabled(false); // returns false * isEnabled(null); // returns false * isEnabled(); // returns false */ function isEnabled(value) { - return value?.toLowerCase()?.trim() === 'true'; + if (typeof value === 'boolean') { + return value; + } + if (typeof value === 'string') { + return value.toLowerCase().trim() === 'true'; + } + return false; } module.exports = { diff --git a/api/server/utils/handleText.spec.js b/api/server/utils/handleText.spec.js new file mode 100644 index 0000000000..ea440a89a5 --- /dev/null +++ b/api/server/utils/handleText.spec.js @@ -0,0 +1,51 @@ +const { isEnabled } = require('./handleText'); + +describe('isEnabled', () => { + test('should return true when input is "true"', () => { + expect(isEnabled('true')).toBe(true); + }); + + test('should return true when input is "TRUE"', () => { + expect(isEnabled('TRUE')).toBe(true); + }); + + test('should return true when input is true', () => { + expect(isEnabled(true)).toBe(true); + }); + + test('should return false when input is "false"', () => { + expect(isEnabled('false')).toBe(false); + }); + + test('should return false when input is false', () => { + expect(isEnabled(false)).toBe(false); + }); + + test('should return false when input is null', () => { + expect(isEnabled(null)).toBe(false); + }); + + test('should return false when input is undefined', () => { + expect(isEnabled()).toBe(false); + }); + + test('should return false when input is an empty string', () => { + expect(isEnabled('')).toBe(false); + }); + + test('should return false when input is a whitespace string', () => { + expect(isEnabled(' ')).toBe(false); + }); + + test('should return false when input is a number', () => { + expect(isEnabled(123)).toBe(false); + }); + + test('should return false when input is an object', () => { + expect(isEnabled({})).toBe(false); + }); + + test('should return false when input is an array', () => { + expect(isEnabled([])).toBe(false); + }); +}); diff --git a/api/server/utils/math.js b/api/server/utils/math.js index 12c12c8ccd..3cd0929890 100644 --- a/api/server/utils/math.js +++ b/api/server/utils/math.js @@ -38,8 +38,7 @@ function math(str, fallbackValue) { if (fallback) { return fallbackValue; } - console.error('str', str); - throw new Error(`str did not evaluate to a number but to a ${typeof value}`); + throw new Error(`[math] str did not evaluate to a number but to a ${typeof value}`); } return value; diff --git a/api/server/utils/sendEmail.js b/api/server/utils/sendEmail.js index d230a90db8..2f85f89dcd 100644 --- a/api/server/utils/sendEmail.js +++ b/api/server/utils/sendEmail.js @@ -1,7 +1,8 @@ -const nodemailer = require('nodemailer'); -const handlebars = require('handlebars'); const fs = require('fs'); const path = require('path'); +const nodemailer = require('nodemailer'); +const handlebars = require('handlebars'); +const logger = require('~/config/winston'); const sendEmail = async (email, subject, payload, template) => { try { @@ -58,15 +59,15 @@ const sendEmail = async (email, subject, payload, template) => { // Send email transporter.sendMail(options(), (error, info) => { if (error) { - console.log(error); + logger.error('[sendEmail]', error); return error; } else { - console.log(info); + logger.debug('[sendEmail]', info); return info; } }); } catch (error) { - console.log(error); + logger.error('[sendEmail]', error); return error; } }; diff --git a/api/server/utils/streamResponse.js b/api/server/utils/streamResponse.js index 2aaf9f6531..85d35f2c55 100644 --- a/api/server/utils/streamResponse.js +++ b/api/server/utils/streamResponse.js @@ -1,5 +1,5 @@ const crypto = require('crypto'); -const { saveMessage } = require('../../models/Message'); +const { saveMessage } = require('~/models/Message'); /** * Sends error data in Server Sent Events format and ends the response. diff --git a/api/strategies/discordStrategy.js b/api/strategies/discordStrategy.js index 9a83c5b9ff..c6fdde6d8c 100644 --- a/api/strategies/discordStrategy.js +++ b/api/strategies/discordStrategy.js @@ -1,5 +1,6 @@ const { Strategy: DiscordStrategy } = require('passport-discord'); -const User = require('../models/User'); +const { logger } = require('~/config'); +const User = require('~/models/User'); const discordLogin = async (accessToken, refreshToken, profile, cb) => { try { @@ -40,7 +41,7 @@ const discordLogin = async (accessToken, refreshToken, profile, cb) => { message: 'User not found.', }); } catch (err) { - console.error(err); + logger.error('[discordLogin]', err); return cb(err); } }; diff --git a/api/strategies/facebookStrategy.js b/api/strategies/facebookStrategy.js index b757f0a7cd..bb175a099c 100644 --- a/api/strategies/facebookStrategy.js +++ b/api/strategies/facebookStrategy.js @@ -1,5 +1,6 @@ const FacebookStrategy = require('passport-facebook').Strategy; -const User = require('../models/User'); +const { logger } = require('~/config'); +const User = require('~/models/User'); const facebookLogin = async (accessToken, refreshToken, profile, cb) => { try { @@ -32,7 +33,7 @@ const facebookLogin = async (accessToken, refreshToken, profile, cb) => { message: 'User not found.', }); } catch (err) { - console.error(err); + logger.error('[facebookLogin]', err); return cb(err); } }; diff --git a/api/strategies/githubStrategy.js b/api/strategies/githubStrategy.js index 2c8087203a..3962c58e50 100644 --- a/api/strategies/githubStrategy.js +++ b/api/strategies/githubStrategy.js @@ -1,5 +1,6 @@ const { Strategy: GitHubStrategy } = require('passport-github2'); -const User = require('../models/User'); +const { logger } = require('~/config'); +const User = require('~/models/User'); const githubLogin = async (accessToken, refreshToken, profile, cb) => { try { @@ -29,7 +30,7 @@ const githubLogin = async (accessToken, refreshToken, profile, cb) => { return cb(null, false, { message: 'User not found.' }); } catch (err) { - console.error(err); + logger.error('[githubLogin]', err); return cb(err); } }; diff --git a/api/strategies/googleStrategy.js b/api/strategies/googleStrategy.js index c41142f5ce..e65c5403f4 100644 --- a/api/strategies/googleStrategy.js +++ b/api/strategies/googleStrategy.js @@ -1,5 +1,6 @@ const { Strategy: GoogleStrategy } = require('passport-google-oauth20'); -const User = require('../models/User'); +const { logger } = require('~/config'); +const User = require('~/models/User'); const googleLogin = async (accessToken, refreshToken, profile, cb) => { try { @@ -29,7 +30,7 @@ const googleLogin = async (accessToken, refreshToken, profile, cb) => { return cb(null, false, { message: 'User not found.' }); } catch (err) { - console.error(err); + logger.error('[googleLogin]', err); return cb(err); } }; diff --git a/api/strategies/joseStrategy.js b/api/strategies/joseStrategy.js index a5ee5ee3ae..83cad23ddf 100644 --- a/api/strategies/joseStrategy.js +++ b/api/strategies/joseStrategy.js @@ -1,9 +1,11 @@ -/* const jose = require('jose'); -* No longer using this strategy as Bun now supports JWTs natively. +/* +const jose = require('jose'); +const { logger } = require('~/config'); +// No longer using this strategy as Bun now supports JWTs natively. const passportCustom = require('passport-custom'); const CustomStrategy = passportCustom.Strategy; -const User = require('../models/User'); +const User = require('~/models/User'); const joseLogin = async () => new CustomStrategy(async (req, done) => { @@ -23,15 +25,15 @@ const joseLogin = async () => if (user) { done(null, user); } else { - console.log('JoseJwtStrategy => no user found'); + logger.debug('JoseJwtStrategy => no user found'); done(null, false, { message: 'No user found' }); } } catch (err) { if (err?.code === 'ERR_JWT_EXPIRED') { - console.error('JoseJwtStrategy => token expired'); + logger.error('JoseJwtStrategy => token expired'); } else { - console.error('JoseJwtStrategy => error'); - console.error(err); + logger.error('JoseJwtStrategy => error'); + logger.error(err); } done(null, false, { message: 'Invalid token' }); } diff --git a/api/strategies/jwtStrategy.js b/api/strategies/jwtStrategy.js index d27124d21b..8079ac3bce 100644 --- a/api/strategies/jwtStrategy.js +++ b/api/strategies/jwtStrategy.js @@ -1,5 +1,6 @@ const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt'); -const User = require('../models/User'); +const { logger } = require('~/config'); +const User = require('~/models/User'); // JWT strategy const jwtLogin = async () => @@ -10,11 +11,11 @@ const jwtLogin = async () => }, async (payload, done) => { try { - const user = await User.findById(payload.id); + const user = await User.findById(payload?.id); if (user) { done(null, user); } else { - console.log('JwtStrategy => no user found'); + logger.warn('[jwtLogin] JwtStrategy => no user found: ' + payload?.id); done(null, false); } } catch (err) { diff --git a/api/strategies/openidStrategy.js b/api/strategies/openidStrategy.js index 8b9b14108f..7219f24ba4 100644 --- a/api/strategies/openidStrategy.js +++ b/api/strategies/openidStrategy.js @@ -1,16 +1,16 @@ -const passport = require('passport'); -const { Issuer, Strategy: OpenIDStrategy } = require('openid-client'); -const axios = require('axios'); const fs = require('fs'); const path = require('path'); - -const User = require('../models/User'); +const axios = require('axios'); +const passport = require('passport'); +const { Issuer, Strategy: OpenIDStrategy } = require('openid-client'); +const { logger } = require('~/config'); +const User = require('~/models/User'); let crypto; try { crypto = require('node:crypto'); } catch (err) { - console.error('crypto support is disabled!'); + logger.error('[openidStrategy] crypto support is disabled!', err); } const downloadImage = async (url, imagePath, accessToken) => { @@ -29,7 +29,9 @@ const downloadImage = async (url, imagePath, accessToken) => { return `/images/openid/${fileName}`; } catch (error) { - console.error(`Error downloading image at URL "${url}": ${error}`); + logger.error( + `[openidStrategy] downloadImage: Error downloading image at URL "${url}": ${error}`, + ); return ''; } }; @@ -130,7 +132,7 @@ async function setupOpenId() { passport.use('openid', openidLogin); } catch (err) { - console.error(err); + logger.error('[openidStrategy]', err); } } diff --git a/api/test/__mocks__/logger.js b/api/test/__mocks__/logger.js new file mode 100644 index 0000000000..455ada0de0 --- /dev/null +++ b/api/test/__mocks__/logger.js @@ -0,0 +1,10 @@ +jest.mock('~/config', () => { + return { + logger: { + info: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + }, + }; +}); diff --git a/api/typedefs.js b/api/typedefs.js index c12254e32d..1ab9f64571 100644 --- a/api/typedefs.js +++ b/api/typedefs.js @@ -111,6 +111,32 @@ * @memberof typedefs */ +/** + * @exports UserMessageContent + * @typedef {Object} UserMessageContent + * @property {Object[]} content - The content of the message in an array of text and/or images. + * @property {string} content[].type - The type of content, either 'text' or 'image_file'. + * @property {Object} [content[].text] - The text content, present if type is 'text'. + * @property {string} content[].text.value - The data that makes up the text. + * @property {Object} [content[].image_url] - The image file content, present if type is 'image_file'. + * @property {string} content[].image_url.url - The File ID of the image in the message content. + * @property {'auto' | 'low' | 'high'} content[].image_url.detail: 'auto' - the quality to use for the image, either 'auto', 'low', or 'high'. + * @memberof typedefs + */ + +/** + * Represents a message payload with various potential properties, + * including roles, sender information, and content. + * + * @typedef {Object} PayloadMessage + * @property {string} [role] - The role of the message sender (e.g., 'user', 'assistant'). + * @property {string} [name] - The name associated with the message. + * @property {string} [sender] - The sender of the message. + * @property {string} [text] - The text content of the message. + * @property {(string|Array)} [content] - The content of the message, which could be a string or an array of the 'content' property from the Message type. + * @memberof typedefs + */ + /** * @exports FunctionTool * @typedef {Object} FunctionTool diff --git a/api/utils/findMessageContent.js b/api/utils/findMessageContent.js index c506435031..6ee5166348 100644 --- a/api/utils/findMessageContent.js +++ b/api/utils/findMessageContent.js @@ -1,3 +1,5 @@ +const { logger } = require('~/config'); + function findContent(obj) { if (obj && typeof obj === 'object') { if ('kwargs' in obj && 'content' in obj.kwargs) { @@ -21,7 +23,7 @@ function findMessageContent(message) { try { jsonObjectOrArray = JSON.parse(jsonString); } catch (error) { - console.error('Failed to parse JSON:', error); + logger.error('[findMessageContent] Failed to parse JSON:', error); return null; } diff --git a/docs/features/logging_system.md b/docs/features/logging_system.md new file mode 100644 index 0000000000..23ada6a73a --- /dev/null +++ b/docs/features/logging_system.md @@ -0,0 +1,28 @@ +### Logging + +LibreChat has central logging built into its backend (api). + +Log files are saved in `/api/logs`. Error logs are saved by default. Debug logs are enabled by default but can be turned off if not desired. + +This allows you to monitor your server through external tools that inspect log files, such as [the ELK stack](https://aws.amazon.com/what-is/elk-stack/). + +Debug logs are essential for developer work and fixing issues. If you encounter any problems running LibreChat, reproduce as close as possible, and [report the issue](https://github.com/danny-avila/LibreChat/issues) with your logs found in `./api/logs/debug-%DATE%.log`. + +Errors logs are also saved in the same location: `./api/logs/error-%DATE%.log`. If you have meilisearch configured, there is a separate log file for this as well. + +> Note: Logs are rotated on a 14-day basis, so you will generate 1 error log file, 1 debug log file, and 1 meiliSync log file per 14 days. +> Errors will also be present in debug log files as well, but provide stack traces and more detail in the error log files. + +Keep debug logs enabled with the following environment variable. Even if you never set this variable, debug logs will be generated, but you have the option to disable them by setting it to `FALSE`. + +```bash +DEBUG_LOGGING=TRUE +``` + +For verbose server output in the console/terminal, you can also set the following: + +```bash +DEBUG_CONSOLE=TRUE +``` + +This is not recommend, however, as the outputs can be quite verbose. It's disabled by default and should be enabled sparingly. \ No newline at end of file diff --git a/docs/install/dotenv.md b/docs/install/dotenv.md index 629c920779..98ad009b4e 100644 --- a/docs/install/dotenv.md +++ b/docs/install/dotenv.md @@ -15,6 +15,29 @@ APP_TITLE=LibreChat CUSTOM_FOOTER="My custom footer" ``` +### Logging + +LibreChat has built-in central logging. + +- Debug logging is enabled by default and crucial for development. +- To report issues, reproduce the error and submit logs from `./api/logs/debug-%DATE%.log` at [LibreChat GitHub Issues](https://github.com/danny-avila/LibreChat/issues). +- Error logs are stored in the same location. +- Keep debug logs active by default or disable them by setting `DEBUG_LOGGING=FALSE` in the environment variable. +- For more information about this feature, read our docs: https://docs.librechat.ai/features/logging_system.html + +```bash +DEBUG_LOGGING=TRUE +``` + +- Enable verbose server output in the console with `DEBUG_CONSOLE=TRUE`, though it's not recommended due to high verbosity. + +```bash +DEBUG_CONSOLE=TRUE +``` + +This is not recommend, however, as the outputs can be quite verbose, and so it's disabled by default. + + ### Port - The server will listen to localhost:3080 by default. You can change the target IP as you want. If you want to make this server available externally, for example to share the server with others or expose this from a Docker container, set host to 0.0.0.0 or your external IP interface. diff --git a/mkdocs.yml b/mkdocs.yml index d851203f17..53a3190474 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -108,6 +108,7 @@ nav: - Automated Moderation: 'features/mod_system.md' - Token Usage: 'features/token_usage.md' - Manage Your Database: 'features/manage_your_database.md' + - Logging System: 'features/logging_system.md' - PandoraNext Deployment Guide: 'features/pandoranext.md' - Third-Party Tools: 'features/third_party.md' - Proxy: 'features/proxy.md' diff --git a/package-lock.json b/package-lock.json index 1cbbf411fa..b02470f7ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,9 @@ "jsonwebtoken": "^9.0.0", "keyv": "^4.5.4", "keyv-file": "^0.2.0", + "klona": "^2.0.6", "langchain": "^0.0.186", + "librechat-data-provider": "*", "lodash": "^4.17.21", "meilisearch": "^0.33.0", "module-alias": "^2.2.3", @@ -87,8 +89,10 @@ "pino": "^8.12.1", "sharp": "^0.32.6", "tiktoken": "^1.0.10", + "traverse": "^0.6.7", "ua-parser-js": "^1.0.36", - "winston": "^3.10.0", + "winston": "^3.11.0", + "winston-daily-rotate-file": "^4.7.1", "zod": "^3.22.4" }, "devDependencies": { @@ -12627,6 +12631,14 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "dependencies": { + "moment": "^2.29.1" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -16099,6 +16111,14 @@ "node": ">=6" } }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "engines": { + "node": ">= 8" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -17845,6 +17865,14 @@ "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==" }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/mongodb": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.8.1.tgz", @@ -23590,6 +23618,14 @@ "node": ">=12" } }, + "node_modules/traverse": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", + "integrity": "sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -24871,11 +24907,11 @@ } }, "node_modules/winston": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz", - "integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", + "integrity": "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==", "dependencies": { - "@colors/colors": "1.5.0", + "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", @@ -24891,6 +24927,31 @@ "node": ">= 12.0.0" } }, + "node_modules/winston-daily-rotate-file": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-4.7.1.tgz", + "integrity": "sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA==", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^2.0.1", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-daily-rotate-file/node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/winston-transport": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", @@ -24917,6 +24978,14 @@ "node": ">= 6" } }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/winston/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",