Compare commits
26 Commits
fix/window
...
v0.7.5-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020995514e | ||
|
|
d6c0121b19 | ||
|
|
1a1e6850a3 | ||
|
|
341e086d70 | ||
|
|
0148b9b097 | ||
|
|
748b41eda4 | ||
|
|
d59b62174f | ||
|
|
8c14360263 | ||
|
|
d0dc858e2d | ||
|
|
9cf390c657 | ||
|
|
14199d5521 | ||
|
|
b9197f90c6 | ||
|
|
9ec665dd2c | ||
|
|
136599081c | ||
|
|
a0291ed155 | ||
|
|
618be4bf2b | ||
|
|
79f9cd5a4d | ||
|
|
63b80c3067 | ||
|
|
7536e649d4 | ||
|
|
6936d0059f | ||
|
|
0a359aa705 | ||
|
|
2ce4f66218 | ||
|
|
a0042317b2 | ||
|
|
dc40e577af | ||
|
|
757b6d3275 | ||
|
|
3b61322459 |
20
.env.example
20
.env.example
@@ -111,6 +111,26 @@ ANTHROPIC_API_KEY=user_provided
|
||||
BINGAI_TOKEN=user_provided
|
||||
# BINGAI_HOST=https://cn.bing.com
|
||||
|
||||
#=================#
|
||||
# AWS Bedrock #
|
||||
#=================#
|
||||
|
||||
# BEDROCK_AWS_DEFAULT_REGION=us-east-1 # A default region must be provided
|
||||
# BEDROCK_AWS_ACCESS_KEY_ID=someAccessKey
|
||||
# BEDROCK_AWS_SECRET_ACCESS_KEY=someSecretAccessKey
|
||||
|
||||
# Note: This example list is not meant to be exhaustive. If omitted, all known, supported model IDs will be included for you.
|
||||
# BEDROCK_AWS_MODELS=anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0
|
||||
|
||||
# See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns
|
||||
|
||||
# Notes on specific models:
|
||||
# The following models are not support due to not supporting streaming:
|
||||
# ai21.j2-mid-v1
|
||||
|
||||
# The following models are not support due to not supporting conversation history:
|
||||
# ai21.j2-ultra-v1, cohere.command-text-v14, cohere.command-light-text-v14
|
||||
|
||||
#============#
|
||||
# Google #
|
||||
#============#
|
||||
|
||||
2
.github/workflows/frontend-review.yml
vendored
2
.github/workflows/frontend-review.yml
vendored
@@ -53,4 +53,4 @@ jobs:
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm run test:ci --verbose
|
||||
working-directory: client
|
||||
working-directory: client
|
||||
@@ -1,4 +1,4 @@
|
||||
# v0.7.5-rc1
|
||||
# v0.7.5-rc2
|
||||
|
||||
# Base node image
|
||||
FROM node:20-alpine AS node
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Dockerfile.multi
|
||||
# v0.7.5-rc1
|
||||
# v0.7.5-rc2
|
||||
|
||||
# Base for all builds
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
@@ -42,9 +42,11 @@
|
||||
|
||||
- 🖥️ UI matching ChatGPT, including Dark mode, Streaming, and latest updates
|
||||
- 🤖 AI model selection:
|
||||
- OpenAI, Azure OpenAI, BingAI, ChatGPT, Google Vertex AI, Anthropic (Claude), Plugins, Assistants API (including Azure Assistants)
|
||||
- Anthropic (Claude), AWS Bedrock, OpenAI, Azure OpenAI, BingAI, ChatGPT, Google Vertex AI, Plugins, Assistants API (including Azure Assistants)
|
||||
- ✅ Compatible across both **[Remote & Local AI services](https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints):**
|
||||
- groq, Ollama, Cohere, Mistral AI, Apple MLX, koboldcpp, OpenRouter, together.ai, Perplexity, ShuttleAI, and more
|
||||
- 🪄 Generative UI with **[Code Artifacts](https://youtu.be/GfTj7O4gmd0?si=WJbdnemZpJzBrJo3)**
|
||||
- Create React, HTML code, and Mermaid diagrams right in chat
|
||||
- 💾 Create, Save, & Share Custom Presets
|
||||
- 🔀 Switch between AI Endpoints and Presets, mid-chat
|
||||
- 🔄 Edit, Resubmit, and Continue Messages with Conversation branching
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
const crypto = require('crypto');
|
||||
const fetch = require('node-fetch');
|
||||
const { supportsBalanceCheck, Constants, CacheKeys, Time } = require('librechat-data-provider');
|
||||
const {
|
||||
supportsBalanceCheck,
|
||||
isAgentsEndpoint,
|
||||
paramEndpoints,
|
||||
ErrorTypes,
|
||||
Constants,
|
||||
CacheKeys,
|
||||
Time,
|
||||
} = require('librechat-data-provider');
|
||||
const { getMessages, saveMessage, updateMessage, saveConvo } = require('~/models');
|
||||
const { addSpaceIfNeeded, isEnabled } = require('~/server/utils');
|
||||
const checkBalance = require('~/models/checkBalance');
|
||||
@@ -28,6 +36,12 @@ class BaseClient {
|
||||
this.userMessagePromise;
|
||||
/** @type {ClientDatabaseSavePromise} */
|
||||
this.responsePromise;
|
||||
/** @type {string} */
|
||||
this.user;
|
||||
/** @type {string} */
|
||||
this.conversationId;
|
||||
/** @type {string} */
|
||||
this.responseMessageId;
|
||||
}
|
||||
|
||||
setOptions() {
|
||||
@@ -54,6 +68,17 @@ class BaseClient {
|
||||
throw new Error('Subclasses attempted to call summarizeMessages without implementing it');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getResponseModel() {
|
||||
if (isAgentsEndpoint(this.options.endpoint) && this.options.agent && this.options.agent.id) {
|
||||
return this.options.agent.id;
|
||||
}
|
||||
|
||||
return this.modelOptions.model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method to get the token count for a message. Subclasses must implement this method.
|
||||
* @param {TMessage} responseMessage
|
||||
@@ -155,6 +180,8 @@ class BaseClient {
|
||||
this.currentMessages[this.currentMessages.length - 1].messageId = head;
|
||||
}
|
||||
|
||||
this.responseMessageId = responseMessageId;
|
||||
|
||||
return {
|
||||
...opts,
|
||||
user,
|
||||
@@ -203,6 +230,7 @@ class BaseClient {
|
||||
userMessage,
|
||||
conversationId,
|
||||
responseMessageId,
|
||||
sender: this.sender,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -341,7 +369,12 @@ class BaseClient {
|
||||
};
|
||||
}
|
||||
|
||||
async handleContextStrategy({ instructions, orderedMessages, formattedMessages }) {
|
||||
async handleContextStrategy({
|
||||
instructions,
|
||||
orderedMessages,
|
||||
formattedMessages,
|
||||
buildTokenMap = true,
|
||||
}) {
|
||||
let _instructions;
|
||||
let tokenCount;
|
||||
|
||||
@@ -383,9 +416,10 @@ class BaseClient {
|
||||
|
||||
const latestMessage = orderedWithInstructions[orderedWithInstructions.length - 1];
|
||||
if (payload.length === 0 && !shouldSummarize && latestMessage) {
|
||||
throw new Error(
|
||||
`Prompt token count of ${latestMessage.tokenCount} exceeds max token count of ${this.maxContextTokens}.`,
|
||||
);
|
||||
const info = `${latestMessage.tokenCount} / ${this.maxContextTokens}`;
|
||||
const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`;
|
||||
logger.warn(`Prompt token count exceeds max token count (${info}).`);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (usePrevSummary) {
|
||||
@@ -410,19 +444,23 @@ class BaseClient {
|
||||
maxContextTokens: this.maxContextTokens,
|
||||
});
|
||||
|
||||
let tokenCountMap = orderedWithInstructions.reduce((map, message, index) => {
|
||||
const { messageId } = message;
|
||||
if (!messageId) {
|
||||
/** @type {Record<string, number> | undefined} */
|
||||
let tokenCountMap;
|
||||
if (buildTokenMap) {
|
||||
tokenCountMap = orderedWithInstructions.reduce((map, message, index) => {
|
||||
const { messageId } = message;
|
||||
if (!messageId) {
|
||||
return map;
|
||||
}
|
||||
|
||||
if (shouldSummarize && index === summaryIndex && !usePrevSummary) {
|
||||
map.summaryMessage = { ...summaryMessage, messageId, tokenCount: summaryTokenCount };
|
||||
}
|
||||
|
||||
map[messageId] = orderedWithInstructions[index].tokenCount;
|
||||
return map;
|
||||
}
|
||||
|
||||
if (shouldSummarize && index === summaryIndex && !usePrevSummary) {
|
||||
map.summaryMessage = { ...summaryMessage, messageId, tokenCount: summaryTokenCount };
|
||||
}
|
||||
|
||||
map[messageId] = orderedWithInstructions[index].tokenCount;
|
||||
return map;
|
||||
}, {});
|
||||
}, {});
|
||||
}
|
||||
|
||||
const promptTokens = this.maxContextTokens - remainingContextTokens;
|
||||
|
||||
@@ -524,6 +562,7 @@ class BaseClient {
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {string|string[]|undefined} */
|
||||
const completion = await this.sendCompletion(payload, opts);
|
||||
this.abortController.requestCompleted = true;
|
||||
|
||||
@@ -533,15 +572,23 @@ class BaseClient {
|
||||
parentMessageId: userMessage.messageId,
|
||||
isCreatedByUser: false,
|
||||
isEdited,
|
||||
model: this.modelOptions.model,
|
||||
model: this.getResponseModel(),
|
||||
sender: this.sender,
|
||||
text: addSpaceIfNeeded(generation) + completion,
|
||||
promptTokens,
|
||||
iconURL: this.options.iconURL,
|
||||
endpoint: this.options.endpoint,
|
||||
...(this.metadata ?? {}),
|
||||
};
|
||||
|
||||
if (typeof completion === 'string') {
|
||||
responseMessage.text = addSpaceIfNeeded(generation) + completion;
|
||||
} else if (Array.isArray(completion) && paramEndpoints.has(this.options.endpoint)) {
|
||||
responseMessage.text = '';
|
||||
responseMessage.content = completion;
|
||||
} else if (Array.isArray(completion)) {
|
||||
responseMessage.text = addSpaceIfNeeded(generation) + completion.join('');
|
||||
}
|
||||
|
||||
if (
|
||||
tokenCountMap &&
|
||||
this.recordTokenUsage &&
|
||||
@@ -861,8 +908,12 @@ class BaseClient {
|
||||
|
||||
processValue(nestedValue);
|
||||
}
|
||||
} else {
|
||||
} else if (typeof value === 'string') {
|
||||
numTokens += this.getTokenCount(value);
|
||||
} else if (typeof value === 'number') {
|
||||
numTokens += this.getTokenCount(value.toString());
|
||||
} else if (typeof value === 'boolean') {
|
||||
numTokens += this.getTokenCount(value.toString());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1023,7 +1023,7 @@ ${convo}
|
||||
async chatCompletion({ payload, onProgress, abortController = null }) {
|
||||
let error = null;
|
||||
const errorCallback = (err) => (error = err);
|
||||
let intermediateReply = '';
|
||||
const intermediateReply = [];
|
||||
try {
|
||||
if (!abortController) {
|
||||
abortController = new AbortController();
|
||||
@@ -1217,19 +1217,19 @@ ${convo}
|
||||
}
|
||||
|
||||
if (typeof finalMessage.content !== 'string' || finalMessage.content.trim() === '') {
|
||||
finalChatCompletion.choices[0].message.content = intermediateReply;
|
||||
finalChatCompletion.choices[0].message.content = intermediateReply.join('');
|
||||
}
|
||||
})
|
||||
.on('finalMessage', (message) => {
|
||||
if (message?.role !== 'assistant') {
|
||||
stream.messages.push({ role: 'assistant', content: intermediateReply });
|
||||
stream.messages.push({ role: 'assistant', content: intermediateReply.join('') });
|
||||
UnexpectedRoleError = true;
|
||||
}
|
||||
});
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const token = chunk.choices[0]?.delta?.content || '';
|
||||
intermediateReply += token;
|
||||
intermediateReply.push(token);
|
||||
onProgress(token);
|
||||
if (abortController.signal.aborted) {
|
||||
stream.controller.abort();
|
||||
@@ -1271,7 +1271,7 @@ ${convo}
|
||||
const { choices } = chatCompletion;
|
||||
if (!Array.isArray(choices) || choices.length === 0) {
|
||||
logger.warn('[OpenAIClient] Chat completion response has no choices');
|
||||
return intermediateReply;
|
||||
return intermediateReply.join('');
|
||||
}
|
||||
|
||||
const { message, finish_reason } = choices[0] ?? {};
|
||||
@@ -1281,15 +1281,16 @@ ${convo}
|
||||
|
||||
if (!message) {
|
||||
logger.warn('[OpenAIClient] Message is undefined in chatCompletion response');
|
||||
return intermediateReply;
|
||||
return intermediateReply.join('');
|
||||
}
|
||||
|
||||
if (typeof message.content !== 'string' || message.content.trim() === '') {
|
||||
const reply = intermediateReply.join('');
|
||||
logger.debug(
|
||||
'[OpenAIClient] chatCompletion: using intermediateReply due to empty message.content',
|
||||
{ intermediateReply },
|
||||
{ intermediateReply: reply },
|
||||
);
|
||||
return intermediateReply;
|
||||
return reply;
|
||||
}
|
||||
|
||||
return message.content;
|
||||
@@ -1298,7 +1299,7 @@ ${convo}
|
||||
err?.message?.includes('abort') ||
|
||||
(err instanceof OpenAI.APIError && err?.message?.includes('abort'))
|
||||
) {
|
||||
return intermediateReply;
|
||||
return intermediateReply.join('');
|
||||
}
|
||||
if (
|
||||
err?.message?.includes(
|
||||
@@ -1313,10 +1314,10 @@ ${convo}
|
||||
(err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason'))
|
||||
) {
|
||||
logger.error('[OpenAIClient] Known OpenAI error:', err);
|
||||
return intermediateReply;
|
||||
return intermediateReply.join('');
|
||||
} else if (err instanceof OpenAI.APIError) {
|
||||
if (intermediateReply) {
|
||||
return intermediateReply;
|
||||
if (intermediateReply.length > 0) {
|
||||
return intermediateReply.join('');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const { isEnabled } = require('~/server/utils');
|
||||
* @param {Object} options - The options for creating the LLM.
|
||||
* @param {ModelOptions} options.modelOptions - The options specific to the model, including modelName, temperature, presence_penalty, frequency_penalty, and other model-related settings.
|
||||
* @param {ConfigOptions} options.configOptions - Configuration options for the API requests, including proxy settings and custom headers.
|
||||
* @param {Callbacks} options.callbacks - Callback functions for managing the lifecycle of the LLM, including token buffers, context, and initial message count.
|
||||
* @param {Callbacks} [options.callbacks] - Callback functions for managing the lifecycle of the LLM, including token buffers, context, and initial message count.
|
||||
* @param {boolean} [options.streaming=false] - Determines if the LLM should operate in streaming mode.
|
||||
* @param {string} options.openAIApiKey - The API key for OpenAI, used for authentication.
|
||||
* @param {AzureOptions} [options.azure={}] - Optional Azure-specific configurations. If provided, Azure configurations take precedence over OpenAI configurations.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const { ToolMessage } = require('@langchain/core/messages');
|
||||
const { EModelEndpoint, ContentTypes } = require('librechat-data-provider');
|
||||
const { HumanMessage, AIMessage, SystemMessage } = require('langchain/schema');
|
||||
|
||||
/**
|
||||
@@ -14,11 +15,11 @@ const { HumanMessage, AIMessage, SystemMessage } = require('langchain/schema');
|
||||
*/
|
||||
const formatVisionMessage = ({ message, image_urls, endpoint }) => {
|
||||
if (endpoint === EModelEndpoint.anthropic) {
|
||||
message.content = [...image_urls, { type: 'text', text: message.content }];
|
||||
message.content = [...image_urls, { type: ContentTypes.TEXT, text: message.content }];
|
||||
return message;
|
||||
}
|
||||
|
||||
message.content = [{ type: 'text', text: message.content }, ...image_urls];
|
||||
message.content = [{ type: ContentTypes.TEXT, text: message.content }, ...image_urls];
|
||||
|
||||
return message;
|
||||
};
|
||||
@@ -51,7 +52,7 @@ const formatMessage = ({ message, userName, assistantName, endpoint, langChain =
|
||||
_role = roleMapping[lc_id[2]];
|
||||
}
|
||||
const role = _role ?? (sender && sender?.toLowerCase() === 'user' ? 'user' : 'assistant');
|
||||
const content = text ?? _content ?? '';
|
||||
const content = _content ?? text ?? '';
|
||||
const formattedMessage = {
|
||||
role,
|
||||
content,
|
||||
@@ -131,4 +132,79 @@ const formatFromLangChain = (message) => {
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { formatMessage, formatLangChainMessages, formatFromLangChain };
|
||||
/**
|
||||
* Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
|
||||
*
|
||||
* @param {Array<Partial<TMessage>>} payload - The array of messages to format.
|
||||
* @returns {Array<(HumanMessage|AIMessage|SystemMessage|ToolMessage)>} - The array of formatted LangChain messages, including ToolMessages for tool calls.
|
||||
*/
|
||||
const formatAgentMessages = (payload) => {
|
||||
const messages = [];
|
||||
|
||||
for (const message of payload) {
|
||||
if (message.role !== 'assistant') {
|
||||
messages.push(formatMessage({ message, langChain: true }));
|
||||
continue;
|
||||
}
|
||||
|
||||
let currentContent = [];
|
||||
let lastAIMessage = null;
|
||||
|
||||
for (const part of message.content) {
|
||||
if (part.type === ContentTypes.TEXT && part.tool_call_ids) {
|
||||
// If there's pending content, add it as an AIMessage
|
||||
if (currentContent.length > 0) {
|
||||
messages.push(new AIMessage({ content: currentContent }));
|
||||
currentContent = [];
|
||||
}
|
||||
|
||||
// Create a new AIMessage with this text and prepare for tool calls
|
||||
lastAIMessage = new AIMessage({
|
||||
content: part.text || '',
|
||||
});
|
||||
|
||||
messages.push(lastAIMessage);
|
||||
} else if (part.type === ContentTypes.TOOL_CALL) {
|
||||
if (!lastAIMessage) {
|
||||
throw new Error('Invalid tool call structure: No preceding AIMessage with tool_call_ids');
|
||||
}
|
||||
|
||||
// Note: `tool_calls` list is defined when constructed by `AIMessage` class, and outputs should be excluded from it
|
||||
const { output, args: _args, ...tool_call } = part.tool_call;
|
||||
// TODO: investigate; args as dictionary may need to be provider-or-tool-specific
|
||||
let args = _args;
|
||||
try {
|
||||
args = JSON.parse(args);
|
||||
} catch (e) {
|
||||
// failed to parse, leave as is
|
||||
}
|
||||
tool_call.args = args;
|
||||
lastAIMessage.tool_calls.push(tool_call);
|
||||
|
||||
// Add the corresponding ToolMessage
|
||||
messages.push(
|
||||
new ToolMessage({
|
||||
tool_call_id: tool_call.id,
|
||||
name: tool_call.name,
|
||||
content: output,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
currentContent.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentContent.length > 0) {
|
||||
messages.push(new AIMessage({ content: currentContent }));
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
formatMessage,
|
||||
formatFromLangChain,
|
||||
formatAgentMessages,
|
||||
formatLangChainMessages,
|
||||
};
|
||||
|
||||
@@ -565,11 +565,13 @@ describe('BaseClient', () => {
|
||||
const getReqData = jest.fn();
|
||||
const opts = { getReqData };
|
||||
const response = await TestClient.sendMessage('Hello, world!', opts);
|
||||
expect(getReqData).toHaveBeenCalledWith({
|
||||
userMessage: expect.objectContaining({ text: 'Hello, world!' }),
|
||||
conversationId: response.conversationId,
|
||||
responseMessageId: response.messageId,
|
||||
});
|
||||
expect(getReqData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userMessage: expect.objectContaining({ text: 'Hello, world!' }),
|
||||
conversationId: response.conversationId,
|
||||
responseMessageId: response.messageId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('onStart is called with the correct arguments', async () => {
|
||||
|
||||
78
api/app/clients/tools/structured/TavilySearch.js
Normal file
78
api/app/clients/tools/structured/TavilySearch.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const { z } = require('zod');
|
||||
const { tool } = require('@langchain/core/tools');
|
||||
const { getEnvironmentVariable } = require('@langchain/core/utils/env');
|
||||
|
||||
function createTavilySearchTool(fields = {}) {
|
||||
const envVar = 'TAVILY_API_KEY';
|
||||
const override = fields.override ?? false;
|
||||
const apiKey = fields.apiKey ?? getApiKey(envVar, override);
|
||||
const kwargs = fields?.kwargs ?? {};
|
||||
|
||||
function getApiKey(envVar, override) {
|
||||
const key = getEnvironmentVariable(envVar);
|
||||
if (!key && !override) {
|
||||
throw new Error(`Missing ${envVar} environment variable.`);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
return tool(
|
||||
async (input) => {
|
||||
const { query, ...rest } = input;
|
||||
|
||||
const requestBody = {
|
||||
api_key: apiKey,
|
||||
query,
|
||||
...rest,
|
||||
...kwargs,
|
||||
};
|
||||
|
||||
const response = await fetch('https://api.tavily.com/search', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed with status ${response.status}: ${json.error}`);
|
||||
}
|
||||
|
||||
return JSON.stringify(json);
|
||||
},
|
||||
{
|
||||
name: 'tavily_search_results_json',
|
||||
description:
|
||||
'A search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events.',
|
||||
schema: z.object({
|
||||
query: z.string().min(1).describe('The search query string.'),
|
||||
max_results: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(10)
|
||||
.optional()
|
||||
.describe('The maximum number of search results to return. Defaults to 5.'),
|
||||
search_depth: z
|
||||
.enum(['basic', 'advanced'])
|
||||
.optional()
|
||||
.describe(
|
||||
'The depth of the search, affecting result quality and response time (`basic` or `advanced`). Default is basic for quick results and advanced for indepth high quality results but longer response time. Advanced calls equals 2 requests.',
|
||||
),
|
||||
include_images: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
'Whether to include a list of query-related images in the response. Default is False.',
|
||||
),
|
||||
include_answer: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to include answers in the search results. Default is False.'),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = createTavilySearchTool;
|
||||
@@ -5,17 +5,16 @@ const Action = mongoose.model('action', actionSchema);
|
||||
|
||||
/**
|
||||
* Update an action with new data without overwriting existing properties,
|
||||
* or create a new action if it doesn't exist, within a transaction session if provided.
|
||||
* or create a new action if it doesn't exist.
|
||||
*
|
||||
* @param {Object} searchParams - The search parameters to find the action to update.
|
||||
* @param {string} searchParams.action_id - The ID of the action to update.
|
||||
* @param {string} searchParams.user - The user ID of the action's author.
|
||||
* @param {Object} updateData - An object containing the properties to update.
|
||||
* @param {mongoose.ClientSession} [session] - The transaction session to use.
|
||||
* @returns {Promise<Object>} The updated or newly created action document as a plain object.
|
||||
* @returns {Promise<Action>} The updated or newly created action document as a plain object.
|
||||
*/
|
||||
const updateAction = async (searchParams, updateData, session = null) => {
|
||||
const options = { new: true, upsert: true, session };
|
||||
const updateAction = async (searchParams, updateData) => {
|
||||
const options = { new: true, upsert: true };
|
||||
return await Action.findOneAndUpdate(searchParams, updateData, options).lean();
|
||||
};
|
||||
|
||||
@@ -24,7 +23,7 @@ const updateAction = async (searchParams, updateData, session = null) => {
|
||||
*
|
||||
* @param {Object} searchParams - The search parameters to find matching actions.
|
||||
* @param {boolean} includeSensitive - Flag to include sensitive data in the metadata.
|
||||
* @returns {Promise<Array<Object>>} A promise that resolves to an array of action documents as plain objects.
|
||||
* @returns {Promise<Array<Action>>} A promise that resolves to an array of action documents as plain objects.
|
||||
*/
|
||||
const getActions = async (searchParams, includeSensitive = false) => {
|
||||
const actions = await Action.find(searchParams).lean();
|
||||
@@ -49,31 +48,27 @@ const getActions = async (searchParams, includeSensitive = false) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an action by params, within a transaction session if provided.
|
||||
* Deletes an action by params.
|
||||
*
|
||||
* @param {Object} searchParams - The search parameters to find the action to delete.
|
||||
* @param {string} searchParams.action_id - The ID of the action to delete.
|
||||
* @param {string} searchParams.user - The user ID of the action's author.
|
||||
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional).
|
||||
* @returns {Promise<Object>} A promise that resolves to the deleted action document as a plain object, or null if no document was found.
|
||||
* @returns {Promise<Action>} A promise that resolves to the deleted action document as a plain object, or null if no document was found.
|
||||
*/
|
||||
const deleteAction = async (searchParams, session = null) => {
|
||||
const options = session ? { session } : {};
|
||||
return await Action.findOneAndDelete(searchParams, options).lean();
|
||||
const deleteAction = async (searchParams) => {
|
||||
return await Action.findOneAndDelete(searchParams).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes actions by params, within a transaction session if provided.
|
||||
* Deletes actions by params.
|
||||
*
|
||||
* @param {Object} searchParams - The search parameters to find the actions to delete.
|
||||
* @param {string} searchParams.action_id - The ID of the action(s) to delete.
|
||||
* @param {string} searchParams.user - The user ID of the action's author.
|
||||
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional).
|
||||
* @returns {Promise<Number>} A promise that resolves to the number of deleted action documents.
|
||||
*/
|
||||
const deleteActions = async (searchParams, session = null) => {
|
||||
const options = session ? { session } : {};
|
||||
const result = await Action.deleteMany(searchParams, options);
|
||||
const deleteActions = async (searchParams) => {
|
||||
const result = await Action.deleteMany(searchParams);
|
||||
return result.deletedCount;
|
||||
};
|
||||
|
||||
|
||||
142
api/models/Agent.js
Normal file
142
api/models/Agent.js
Normal file
@@ -0,0 +1,142 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
|
||||
const {
|
||||
getProjectByName,
|
||||
addAgentIdsToProject,
|
||||
removeAgentIdsFromProject,
|
||||
removeAgentFromAllProjects,
|
||||
} = require('./Project');
|
||||
const agentSchema = require('./schema/agent');
|
||||
|
||||
const Agent = mongoose.model('agent', agentSchema);
|
||||
|
||||
/**
|
||||
* Create an agent with the provided data.
|
||||
* @param {Object} agentData - The agent data to create.
|
||||
* @returns {Promise<Agent>} The created agent document as a plain object.
|
||||
* @throws {Error} If the agent creation fails.
|
||||
*/
|
||||
const createAgent = async (agentData) => {
|
||||
return await Agent.create(agentData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an agent document based on the provided ID.
|
||||
*
|
||||
* @param {Object} searchParameter - The search parameters to find the agent to update.
|
||||
* @param {string} searchParameter.id - The ID of the agent to update.
|
||||
* @param {string} searchParameter.author - The user ID of the agent's author.
|
||||
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
|
||||
*/
|
||||
const getAgent = async (searchParameter) => await Agent.findOne(searchParameter).lean();
|
||||
|
||||
/**
|
||||
* Update an agent with new data without overwriting existing
|
||||
* properties, or create a new agent if it doesn't exist.
|
||||
*
|
||||
* @param {Object} searchParameter - The search parameters to find the agent to update.
|
||||
* @param {string} searchParameter.id - The ID of the agent to update.
|
||||
* @param {string} [searchParameter.author] - The user ID of the agent's author.
|
||||
* @param {Object} updateData - An object containing the properties to update.
|
||||
* @returns {Promise<Agent>} The updated or newly created agent document as a plain object.
|
||||
*/
|
||||
const updateAgent = async (searchParameter, updateData) => {
|
||||
const options = { new: true, upsert: true };
|
||||
return await Agent.findOneAndUpdate(searchParameter, updateData, options).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an agent based on the provided ID.
|
||||
*
|
||||
* @param {Object} searchParameter - The search parameters to find the agent to delete.
|
||||
* @param {string} searchParameter.id - The ID of the agent to delete.
|
||||
* @param {string} [searchParameter.author] - The user ID of the agent's author.
|
||||
* @returns {Promise<void>} Resolves when the agent has been successfully deleted.
|
||||
*/
|
||||
const deleteAgent = async (searchParameter) => {
|
||||
const agent = await Agent.findOneAndDelete(searchParameter);
|
||||
if (agent) {
|
||||
await removeAgentFromAllProjects(agent.id);
|
||||
}
|
||||
return agent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all agents.
|
||||
* @param {Object} searchParameter - The search parameters to find matching agents.
|
||||
* @param {string} searchParameter.author - The user ID of the agent's author.
|
||||
* @returns {Promise<Object>} A promise that resolves to an object containing the agents data and pagination info.
|
||||
*/
|
||||
const getListAgents = async (searchParameter) => {
|
||||
const { author, ...otherParams } = searchParameter;
|
||||
|
||||
let query = Object.assign({ author }, otherParams);
|
||||
|
||||
const globalProject = await getProjectByName(GLOBAL_PROJECT_NAME, ['agentIds']);
|
||||
if (globalProject && (globalProject.agentIds?.length ?? 0) > 0) {
|
||||
const globalQuery = { id: { $in: globalProject.agentIds }, ...otherParams };
|
||||
delete globalQuery.author;
|
||||
query = { $or: [globalQuery, query] };
|
||||
}
|
||||
|
||||
const agents = await Agent.find(query, {
|
||||
id: 1,
|
||||
name: 1,
|
||||
avatar: 1,
|
||||
projectIds: 1,
|
||||
}).lean();
|
||||
|
||||
const hasMore = agents.length > 0;
|
||||
const firstId = agents.length > 0 ? agents[0].id : null;
|
||||
const lastId = agents.length > 0 ? agents[agents.length - 1].id : null;
|
||||
|
||||
return {
|
||||
data: agents,
|
||||
has_more: hasMore,
|
||||
first_id: firstId,
|
||||
last_id: lastId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the projects associated with an agent, adding and removing project IDs as specified.
|
||||
* This function also updates the corresponding projects to include or exclude the agent ID.
|
||||
*
|
||||
* @param {string} agentId - The ID of the agent to update.
|
||||
* @param {string[]} [projectIds] - Array of project IDs to add to the agent.
|
||||
* @param {string[]} [removeProjectIds] - Array of project IDs to remove from the agent.
|
||||
* @returns {Promise<MongoAgent>} The updated agent document.
|
||||
* @throws {Error} If there's an error updating the agent or projects.
|
||||
*/
|
||||
const updateAgentProjects = async (agentId, projectIds, removeProjectIds) => {
|
||||
const updateOps = {};
|
||||
|
||||
if (removeProjectIds && removeProjectIds.length > 0) {
|
||||
for (const projectId of removeProjectIds) {
|
||||
await removeAgentIdsFromProject(projectId, [agentId]);
|
||||
}
|
||||
updateOps.$pull = { projectIds: { $in: removeProjectIds } };
|
||||
}
|
||||
|
||||
if (projectIds && projectIds.length > 0) {
|
||||
for (const projectId of projectIds) {
|
||||
await addAgentIdsToProject(projectId, [agentId]);
|
||||
}
|
||||
updateOps.$addToSet = { projectIds: { $each: projectIds } };
|
||||
}
|
||||
|
||||
if (Object.keys(updateOps).length === 0) {
|
||||
return await getAgent({ id: agentId });
|
||||
}
|
||||
|
||||
return await updateAgent({ id: agentId }, updateOps);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createAgent,
|
||||
getAgent,
|
||||
updateAgent,
|
||||
deleteAgent,
|
||||
getListAgents,
|
||||
updateAgentProjects,
|
||||
};
|
||||
@@ -5,17 +5,16 @@ const Assistant = mongoose.model('assistant', assistantSchema);
|
||||
|
||||
/**
|
||||
* Update an assistant with new data without overwriting existing properties,
|
||||
* or create a new assistant if it doesn't exist, within a transaction session if provided.
|
||||
* or create a new assistant if it doesn't exist.
|
||||
*
|
||||
* @param {Object} searchParams - The search parameters to find the assistant to update.
|
||||
* @param {string} searchParams.assistant_id - The ID of the assistant to update.
|
||||
* @param {string} searchParams.user - The user ID of the assistant's author.
|
||||
* @param {Object} updateData - An object containing the properties to update.
|
||||
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional).
|
||||
* @returns {Promise<Object>} The updated or newly created assistant document as a plain object.
|
||||
* @returns {Promise<AssistantDocument>} The updated or newly created assistant document as a plain object.
|
||||
*/
|
||||
const updateAssistantDoc = async (searchParams, updateData, session = null) => {
|
||||
const options = { new: true, upsert: true, session };
|
||||
const updateAssistantDoc = async (searchParams, updateData) => {
|
||||
const options = { new: true, upsert: true };
|
||||
return await Assistant.findOneAndUpdate(searchParams, updateData, options).lean();
|
||||
};
|
||||
|
||||
@@ -25,7 +24,7 @@ const updateAssistantDoc = async (searchParams, updateData, session = null) => {
|
||||
* @param {Object} searchParams - The search parameters to find the assistant to update.
|
||||
* @param {string} searchParams.assistant_id - The ID of the assistant to update.
|
||||
* @param {string} searchParams.user - The user ID of the assistant's author.
|
||||
* @returns {Promise<Object|null>} The assistant document as a plain object, or null if not found.
|
||||
* @returns {Promise<AssistantDocument|null>} The assistant document as a plain object, or null if not found.
|
||||
*/
|
||||
const getAssistant = async (searchParams) => await Assistant.findOne(searchParams).lean();
|
||||
|
||||
@@ -33,10 +32,17 @@ const getAssistant = async (searchParams) => await Assistant.findOne(searchParam
|
||||
* Retrieves all assistants that match the given search parameters.
|
||||
*
|
||||
* @param {Object} searchParams - The search parameters to find matching assistants.
|
||||
* @returns {Promise<Array<Object>>} A promise that resolves to an array of action documents as plain objects.
|
||||
* @param {Object} [select] - Optional. Specifies which document fields to include or exclude.
|
||||
* @returns {Promise<Array<AssistantDocument>>} A promise that resolves to an array of assistant documents as plain objects.
|
||||
*/
|
||||
const getAssistants = async (searchParams) => {
|
||||
return await Assistant.find(searchParams).lean();
|
||||
const getAssistants = async (searchParams, select = null) => {
|
||||
let query = Assistant.find(searchParams);
|
||||
|
||||
if (select) {
|
||||
query = query.select(select);
|
||||
}
|
||||
|
||||
return await query.lean();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,82 +35,34 @@ const idSchema = z.string().uuid();
|
||||
* @throws {Error} If there is an error in saving the message.
|
||||
*/
|
||||
async function saveMessage(req, params, metadata) {
|
||||
if (!req?.user?.id) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const validConvoId = idSchema.safeParse(params.conversationId);
|
||||
if (!validConvoId.success) {
|
||||
logger.warn(`Invalid conversation ID: ${params.conversationId}`);
|
||||
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
|
||||
logger.info(`---Invalid conversation ID Params: ${JSON.stringify(params, null, 2)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!req || !req.user || !req.user.id) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const {
|
||||
text,
|
||||
error,
|
||||
model,
|
||||
files,
|
||||
plugin,
|
||||
sender,
|
||||
plugins,
|
||||
iconURL,
|
||||
endpoint,
|
||||
isEdited,
|
||||
messageId,
|
||||
unfinished,
|
||||
tokenCount,
|
||||
newMessageId,
|
||||
finish_reason,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
isCreatedByUser,
|
||||
} = params;
|
||||
|
||||
const validConvoId = idSchema.safeParse(conversationId);
|
||||
if (!validConvoId.success) {
|
||||
logger.warn(`Invalid conversation ID: ${conversationId}`);
|
||||
if (metadata && metadata?.context) {
|
||||
logger.info(`---\`saveMessage\` context: ${metadata.context}`);
|
||||
}
|
||||
|
||||
logger.info(`---Invalid conversation ID Params:
|
||||
|
||||
${JSON.stringify(params, null, 2)}
|
||||
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = {
|
||||
...params,
|
||||
user: req.user.id,
|
||||
iconURL,
|
||||
endpoint,
|
||||
messageId: newMessageId || messageId,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender,
|
||||
text,
|
||||
isCreatedByUser,
|
||||
isEdited,
|
||||
finish_reason,
|
||||
error,
|
||||
unfinished,
|
||||
tokenCount,
|
||||
plugin,
|
||||
plugins,
|
||||
model,
|
||||
messageId: params.newMessageId || params.messageId,
|
||||
};
|
||||
|
||||
if (files) {
|
||||
update.files = files;
|
||||
}
|
||||
|
||||
const message = await Message.findOneAndUpdate({ messageId, user: req.user.id }, update, {
|
||||
upsert: true,
|
||||
new: true,
|
||||
});
|
||||
const message = await Message.findOneAndUpdate(
|
||||
{ messageId: params.messageId, user: req.user.id },
|
||||
update,
|
||||
{ upsert: true, new: true },
|
||||
);
|
||||
|
||||
return message.toObject();
|
||||
} catch (err) {
|
||||
logger.error('Error saving message:', err);
|
||||
if (metadata && metadata?.context) {
|
||||
logger.info(`---\`saveMessage\` context: ${metadata.context}`);
|
||||
}
|
||||
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { model } = require('mongoose');
|
||||
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
|
||||
const projectSchema = require('~/models/schema/projectSchema');
|
||||
|
||||
const Project = model('Project', projectSchema);
|
||||
@@ -33,7 +34,7 @@ const getProjectByName = async function (projectName, fieldsToSelect = null) {
|
||||
const update = { $setOnInsert: { name: projectName } };
|
||||
const options = {
|
||||
new: true,
|
||||
upsert: projectName === 'instance',
|
||||
upsert: projectName === GLOBAL_PROJECT_NAME,
|
||||
lean: true,
|
||||
select: fieldsToSelect,
|
||||
};
|
||||
@@ -81,10 +82,55 @@ const removeGroupFromAllProjects = async (promptGroupId) => {
|
||||
await Project.updateMany({}, { $pull: { promptGroupIds: promptGroupId } });
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an array of agent IDs to a project's agentIds array, ensuring uniqueness.
|
||||
*
|
||||
* @param {string} projectId - The ID of the project to update.
|
||||
* @param {string[]} agentIds - The array of agent IDs to add to the project.
|
||||
* @returns {Promise<MongoProject>} The updated project document.
|
||||
*/
|
||||
const addAgentIdsToProject = async function (projectId, agentIds) {
|
||||
return await Project.findByIdAndUpdate(
|
||||
projectId,
|
||||
{ $addToSet: { agentIds: { $each: agentIds } } },
|
||||
{ new: true },
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an array of agent IDs from a project's agentIds array.
|
||||
*
|
||||
* @param {string} projectId - The ID of the project to update.
|
||||
* @param {string[]} agentIds - The array of agent IDs to remove from the project.
|
||||
* @returns {Promise<MongoProject>} The updated project document.
|
||||
*/
|
||||
const removeAgentIdsFromProject = async function (projectId, agentIds) {
|
||||
return await Project.findByIdAndUpdate(
|
||||
projectId,
|
||||
{ $pull: { agentIds: { $in: agentIds } } },
|
||||
{ new: true },
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an agent ID from all projects.
|
||||
*
|
||||
* @param {string} agentId - The ID of the agent to remove from projects.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const removeAgentFromAllProjects = async (agentId) => {
|
||||
await Project.updateMany({}, { $pull: { agentIds: agentId } });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getProjectById,
|
||||
getProjectByName,
|
||||
/* prompts */
|
||||
addGroupIdsToProject,
|
||||
removeGroupIdsFromProject,
|
||||
removeGroupFromAllProjects,
|
||||
/* agents */
|
||||
addAgentIdsToProject,
|
||||
removeAgentIdsFromProject,
|
||||
removeAgentFromAllProjects,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { SystemRoles, SystemCategories } = require('librechat-data-provider');
|
||||
const { SystemRoles, SystemCategories, Constants } = require('librechat-data-provider');
|
||||
const {
|
||||
getProjectByName,
|
||||
addGroupIdsToProject,
|
||||
@@ -123,7 +123,7 @@ const getAllPromptGroups = async (req, filter) => {
|
||||
let combinedQuery = query;
|
||||
|
||||
if (searchShared) {
|
||||
const project = await getProjectByName('instance', 'promptGroupIds');
|
||||
const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds');
|
||||
if (project && project.promptGroupIds.length > 0) {
|
||||
const projectQuery = { _id: { $in: project.promptGroupIds }, ...query };
|
||||
delete projectQuery.author;
|
||||
@@ -177,7 +177,7 @@ const getPromptGroups = async (req, filter) => {
|
||||
|
||||
if (searchShared) {
|
||||
// const projects = req.user.projects || []; // TODO: handle multiple projects
|
||||
const project = await getProjectByName('instance', 'promptGroupIds');
|
||||
const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds');
|
||||
if (project && project.promptGroupIds.length > 0) {
|
||||
const projectQuery = { _id: { $in: project.promptGroupIds }, ...query };
|
||||
delete projectQuery.author;
|
||||
|
||||
@@ -4,8 +4,10 @@ const {
|
||||
roleDefaults,
|
||||
PermissionTypes,
|
||||
removeNullishValues,
|
||||
agentPermissionsSchema,
|
||||
promptPermissionsSchema,
|
||||
bookmarkPermissionsSchema,
|
||||
multiConvoPermissionsSchema,
|
||||
} = require('librechat-data-provider');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const Role = require('~/models/schema/roleSchema');
|
||||
@@ -71,8 +73,10 @@ const updateRoleByName = async function (roleName, updates) {
|
||||
};
|
||||
|
||||
const permissionSchemas = {
|
||||
[PermissionTypes.AGENTS]: agentPermissionsSchema,
|
||||
[PermissionTypes.PROMPTS]: promptPermissionsSchema,
|
||||
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema,
|
||||
[PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -130,6 +134,7 @@ async function updateAccessPermissions(roleName, permissionsUpdate) {
|
||||
/**
|
||||
* Initialize default roles in the system.
|
||||
* Creates the default roles (ADMIN, USER) if they don't exist in the database.
|
||||
* Updates existing roles with new permission types if they're missing.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@@ -137,14 +142,27 @@ const initializeRoles = async function () {
|
||||
const defaultRoles = [SystemRoles.ADMIN, SystemRoles.USER];
|
||||
|
||||
for (const roleName of defaultRoles) {
|
||||
let role = await Role.findOne({ name: roleName }).select('name').lean();
|
||||
let role = await Role.findOne({ name: roleName });
|
||||
|
||||
if (!role) {
|
||||
// Create new role if it doesn't exist
|
||||
role = new Role(roleDefaults[roleName]);
|
||||
await role.save();
|
||||
} else {
|
||||
// Add missing permission types
|
||||
let isUpdated = false;
|
||||
for (const permType of Object.values(PermissionTypes)) {
|
||||
if (!role[permType]) {
|
||||
role[permType] = roleDefaults[roleName][permType];
|
||||
isUpdated = true;
|
||||
}
|
||||
}
|
||||
if (isUpdated) {
|
||||
await role.save();
|
||||
}
|
||||
}
|
||||
await role.save();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getRoleByName,
|
||||
initializeRoles,
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
const { SystemRoles, PermissionTypes } = require('librechat-data-provider');
|
||||
const Role = require('~/models/schema/roleSchema');
|
||||
const { updateAccessPermissions } = require('~/models/Role');
|
||||
const {
|
||||
SystemRoles,
|
||||
PermissionTypes,
|
||||
roleDefaults,
|
||||
Permissions,
|
||||
} = require('librechat-data-provider');
|
||||
const { updateAccessPermissions, initializeRoles } = require('~/models/Role');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const Role = require('~/models/schema/roleSchema');
|
||||
|
||||
// Mock the cache
|
||||
jest.mock('~/cache/getLogStores', () => {
|
||||
@@ -194,4 +199,222 @@ describe('updateAccessPermissions', () => {
|
||||
SHARED_GLOBAL: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update MULTI_CONVO permissions', async () => {
|
||||
await new Role({
|
||||
name: SystemRoles.USER,
|
||||
[PermissionTypes.MULTI_CONVO]: {
|
||||
USE: false,
|
||||
},
|
||||
}).save();
|
||||
|
||||
await updateAccessPermissions(SystemRoles.USER, {
|
||||
[PermissionTypes.MULTI_CONVO]: {
|
||||
USE: true,
|
||||
},
|
||||
});
|
||||
|
||||
const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean();
|
||||
expect(updatedRole[PermissionTypes.MULTI_CONVO]).toEqual({
|
||||
USE: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update MULTI_CONVO permissions along with other permission types', async () => {
|
||||
await new Role({
|
||||
name: SystemRoles.USER,
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
CREATE: true,
|
||||
USE: true,
|
||||
SHARED_GLOBAL: false,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: {
|
||||
USE: false,
|
||||
},
|
||||
}).save();
|
||||
|
||||
await updateAccessPermissions(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { SHARED_GLOBAL: true },
|
||||
[PermissionTypes.MULTI_CONVO]: { USE: true },
|
||||
});
|
||||
|
||||
const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean();
|
||||
expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({
|
||||
CREATE: true,
|
||||
USE: true,
|
||||
SHARED_GLOBAL: true,
|
||||
});
|
||||
expect(updatedRole[PermissionTypes.MULTI_CONVO]).toEqual({
|
||||
USE: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update MULTI_CONVO permissions when no changes are needed', async () => {
|
||||
await new Role({
|
||||
name: SystemRoles.USER,
|
||||
[PermissionTypes.MULTI_CONVO]: {
|
||||
USE: true,
|
||||
},
|
||||
}).save();
|
||||
|
||||
await updateAccessPermissions(SystemRoles.USER, {
|
||||
[PermissionTypes.MULTI_CONVO]: {
|
||||
USE: true,
|
||||
},
|
||||
});
|
||||
|
||||
const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean();
|
||||
expect(updatedRole[PermissionTypes.MULTI_CONVO]).toEqual({
|
||||
USE: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeRoles', () => {
|
||||
beforeEach(async () => {
|
||||
await Role.deleteMany({});
|
||||
});
|
||||
|
||||
it('should create default roles if they do not exist', async () => {
|
||||
await initializeRoles();
|
||||
|
||||
const adminRole = await Role.findOne({ name: SystemRoles.ADMIN }).lean();
|
||||
const userRole = await Role.findOne({ name: SystemRoles.USER }).lean();
|
||||
|
||||
expect(adminRole).toBeTruthy();
|
||||
expect(userRole).toBeTruthy();
|
||||
|
||||
// Check if all permission types exist
|
||||
Object.values(PermissionTypes).forEach((permType) => {
|
||||
expect(adminRole[permType]).toBeDefined();
|
||||
expect(userRole[permType]).toBeDefined();
|
||||
});
|
||||
|
||||
// Check if permissions match defaults (example for ADMIN role)
|
||||
expect(adminRole[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBe(true);
|
||||
expect(adminRole[PermissionTypes.BOOKMARKS].USE).toBe(true);
|
||||
expect(adminRole[PermissionTypes.AGENTS].CREATE).toBe(true);
|
||||
});
|
||||
|
||||
it('should not modify existing permissions for existing roles', async () => {
|
||||
const customUserRole = {
|
||||
name: SystemRoles.USER,
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: {
|
||||
[Permissions.USE]: false,
|
||||
},
|
||||
};
|
||||
|
||||
await new Role(customUserRole).save();
|
||||
|
||||
await initializeRoles();
|
||||
|
||||
const userRole = await Role.findOne({ name: SystemRoles.USER }).lean();
|
||||
|
||||
expect(userRole[PermissionTypes.PROMPTS]).toEqual(customUserRole[PermissionTypes.PROMPTS]);
|
||||
expect(userRole[PermissionTypes.BOOKMARKS]).toEqual(customUserRole[PermissionTypes.BOOKMARKS]);
|
||||
expect(userRole[PermissionTypes.AGENTS]).toBeDefined();
|
||||
});
|
||||
|
||||
it('should add new permission types to existing roles', async () => {
|
||||
const partialUserRole = {
|
||||
name: SystemRoles.USER,
|
||||
[PermissionTypes.PROMPTS]: roleDefaults[SystemRoles.USER][PermissionTypes.PROMPTS],
|
||||
[PermissionTypes.BOOKMARKS]: roleDefaults[SystemRoles.USER][PermissionTypes.BOOKMARKS],
|
||||
};
|
||||
|
||||
await new Role(partialUserRole).save();
|
||||
|
||||
await initializeRoles();
|
||||
|
||||
const userRole = await Role.findOne({ name: SystemRoles.USER }).lean();
|
||||
|
||||
expect(userRole[PermissionTypes.AGENTS]).toBeDefined();
|
||||
expect(userRole[PermissionTypes.AGENTS].CREATE).toBeDefined();
|
||||
expect(userRole[PermissionTypes.AGENTS].USE).toBeDefined();
|
||||
expect(userRole[PermissionTypes.AGENTS].SHARED_GLOBAL).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle multiple runs without duplicating or modifying data', async () => {
|
||||
await initializeRoles();
|
||||
await initializeRoles();
|
||||
|
||||
const adminRoles = await Role.find({ name: SystemRoles.ADMIN });
|
||||
const userRoles = await Role.find({ name: SystemRoles.USER });
|
||||
|
||||
expect(adminRoles).toHaveLength(1);
|
||||
expect(userRoles).toHaveLength(1);
|
||||
|
||||
const adminRole = adminRoles[0].toObject();
|
||||
const userRole = userRoles[0].toObject();
|
||||
|
||||
// Check if all permission types exist
|
||||
Object.values(PermissionTypes).forEach((permType) => {
|
||||
expect(adminRole[permType]).toBeDefined();
|
||||
expect(userRole[permType]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should update roles with missing permission types from roleDefaults', async () => {
|
||||
const partialAdminRole = {
|
||||
name: SystemRoles.ADMIN,
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: false,
|
||||
[Permissions.CREATE]: false,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: roleDefaults[SystemRoles.ADMIN][PermissionTypes.BOOKMARKS],
|
||||
};
|
||||
|
||||
await new Role(partialAdminRole).save();
|
||||
|
||||
await initializeRoles();
|
||||
|
||||
const adminRole = await Role.findOne({ name: SystemRoles.ADMIN }).lean();
|
||||
|
||||
expect(adminRole[PermissionTypes.PROMPTS]).toEqual(partialAdminRole[PermissionTypes.PROMPTS]);
|
||||
expect(adminRole[PermissionTypes.AGENTS]).toBeDefined();
|
||||
expect(adminRole[PermissionTypes.AGENTS].CREATE).toBeDefined();
|
||||
expect(adminRole[PermissionTypes.AGENTS].USE).toBeDefined();
|
||||
expect(adminRole[PermissionTypes.AGENTS].SHARED_GLOBAL).toBeDefined();
|
||||
});
|
||||
|
||||
it('should include MULTI_CONVO permissions when creating default roles', async () => {
|
||||
await initializeRoles();
|
||||
|
||||
const adminRole = await Role.findOne({ name: SystemRoles.ADMIN }).lean();
|
||||
const userRole = await Role.findOne({ name: SystemRoles.USER }).lean();
|
||||
|
||||
expect(adminRole[PermissionTypes.MULTI_CONVO]).toBeDefined();
|
||||
expect(userRole[PermissionTypes.MULTI_CONVO]).toBeDefined();
|
||||
|
||||
// Check if MULTI_CONVO permissions match defaults
|
||||
expect(adminRole[PermissionTypes.MULTI_CONVO].USE).toBe(
|
||||
roleDefaults[SystemRoles.ADMIN][PermissionTypes.MULTI_CONVO].USE,
|
||||
);
|
||||
expect(userRole[PermissionTypes.MULTI_CONVO].USE).toBe(
|
||||
roleDefaults[SystemRoles.USER][PermissionTypes.MULTI_CONVO].USE,
|
||||
);
|
||||
});
|
||||
|
||||
it('should add MULTI_CONVO permissions to existing roles without them', async () => {
|
||||
const partialUserRole = {
|
||||
name: SystemRoles.USER,
|
||||
[PermissionTypes.PROMPTS]: roleDefaults[SystemRoles.USER][PermissionTypes.PROMPTS],
|
||||
[PermissionTypes.BOOKMARKS]: roleDefaults[SystemRoles.USER][PermissionTypes.BOOKMARKS],
|
||||
};
|
||||
|
||||
await new Role(partialUserRole).save();
|
||||
|
||||
await initializeRoles();
|
||||
|
||||
const userRole = await Role.findOne({ name: SystemRoles.USER }).lean();
|
||||
|
||||
expect(userRole[PermissionTypes.MULTI_CONVO]).toBeDefined();
|
||||
expect(userRole[PermissionTypes.MULTI_CONVO].USE).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ const actionSchema = new Schema({
|
||||
default: 'action_prototype',
|
||||
},
|
||||
settings: Schema.Types.Mixed,
|
||||
agent_id: String,
|
||||
assistant_id: String,
|
||||
metadata: {
|
||||
api_key: String, // private, encrypted
|
||||
|
||||
71
api/models/schema/agent.js
Normal file
71
api/models/schema/agent.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const agentSchema = mongoose.Schema(
|
||||
{
|
||||
id: {
|
||||
type: String,
|
||||
index: true,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
},
|
||||
instructions: {
|
||||
type: String,
|
||||
},
|
||||
avatar: {
|
||||
type: {
|
||||
filepath: String,
|
||||
source: String,
|
||||
},
|
||||
default: undefined,
|
||||
},
|
||||
provider: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
model_parameters: {
|
||||
type: Object,
|
||||
},
|
||||
access_level: {
|
||||
type: Number,
|
||||
},
|
||||
tools: {
|
||||
type: [String],
|
||||
default: undefined,
|
||||
},
|
||||
tool_kwargs: {
|
||||
type: [{ type: mongoose.Schema.Types.Mixed }],
|
||||
},
|
||||
file_ids: {
|
||||
type: [String],
|
||||
default: undefined,
|
||||
},
|
||||
actions: {
|
||||
type: [String],
|
||||
default: undefined,
|
||||
},
|
||||
author: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
projectIds: {
|
||||
type: [mongoose.Schema.Types.ObjectId],
|
||||
ref: 'Project',
|
||||
index: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = agentSchema;
|
||||
@@ -19,6 +19,10 @@ const assistantSchema = mongoose.Schema(
|
||||
},
|
||||
default: undefined,
|
||||
},
|
||||
conversation_starters: {
|
||||
type: [String],
|
||||
default: [],
|
||||
},
|
||||
access_level: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
@@ -13,6 +13,11 @@ const conversationPreset = {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
// for bedrock only
|
||||
region: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
// for azureOpenAI, openAI only
|
||||
chatGptLabel: {
|
||||
type: String,
|
||||
@@ -78,6 +83,9 @@ const conversationPreset = {
|
||||
promptCache: {
|
||||
type: Boolean,
|
||||
},
|
||||
system: {
|
||||
type: String,
|
||||
},
|
||||
// files
|
||||
resendFiles: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -21,6 +21,11 @@ const projectSchema = new Schema(
|
||||
ref: 'PromptGroup',
|
||||
default: [],
|
||||
},
|
||||
agentIds: {
|
||||
type: [String],
|
||||
ref: 'Agent',
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -28,6 +28,26 @@ const roleSchema = new mongoose.Schema({
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.SHARED_GLOBAL]: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
[Permissions.USE]: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
[Permissions.CREATE]: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: {
|
||||
[Permissions.USE]: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Role = mongoose.model('Role', roleSchema);
|
||||
|
||||
@@ -122,7 +122,12 @@ const userSchema = mongoose.Schema(
|
||||
type: Date,
|
||||
expires: 604800, // 7 days in seconds
|
||||
},
|
||||
termsAccepted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
|
||||
@@ -3,38 +3,28 @@ const defaultRate = 6;
|
||||
|
||||
/** AWS Bedrock pricing */
|
||||
const bedrockValues = {
|
||||
'anthropic.claude-3-haiku-20240307-v1:0': { prompt: 0.25, completion: 1.25 },
|
||||
'anthropic.claude-3-sonnet-20240229-v1:0': { prompt: 3.0, completion: 15.0 },
|
||||
'anthropic.claude-3-opus-20240229-v1:0': { prompt: 15.0, completion: 75.0 },
|
||||
'anthropic.claude-3-5-sonnet-20240620-v1:0': { prompt: 3.0, completion: 15.0 },
|
||||
'anthropic.claude-v2:1': { prompt: 8.0, completion: 24.0 },
|
||||
'anthropic.claude-instant-v1': { prompt: 0.8, completion: 2.4 },
|
||||
'meta.llama2-13b-chat-v1': { prompt: 0.75, completion: 1.0 },
|
||||
'meta.llama2-70b-chat-v1': { prompt: 1.95, completion: 2.56 },
|
||||
'meta.llama3-8b-instruct-v1:0': { prompt: 0.3, completion: 0.6 },
|
||||
'meta.llama3-70b-instruct-v1:0': { prompt: 2.65, completion: 3.5 },
|
||||
'meta.llama3-1-8b-instruct-v1:0': { prompt: 0.3, completion: 0.6 },
|
||||
'meta.llama3-1-70b-instruct-v1:0': { prompt: 2.65, completion: 3.5 },
|
||||
'meta.llama3-1-405b-instruct-v1:0': { prompt: 5.32, completion: 16.0 },
|
||||
'mistral.mistral-7b-instruct-v0:2': { prompt: 0.15, completion: 0.2 },
|
||||
'mistral.mistral-small-2402-v1:0': { prompt: 0.15, completion: 0.2 },
|
||||
'mistral.mixtral-8x7b-instruct-v0:1': { prompt: 0.45, completion: 0.7 },
|
||||
'mistral.mistral-large-2402-v1:0': { prompt: 4.0, completion: 12.0 },
|
||||
'mistral.mistral-large-2407-v1:0': { prompt: 3.0, completion: 9.0 },
|
||||
'cohere.command-text-v14': { prompt: 1.5, completion: 2.0 },
|
||||
'cohere.command-light-text-v14': { prompt: 0.3, completion: 0.6 },
|
||||
'cohere.command-r-v1:0': { prompt: 0.5, completion: 1.5 },
|
||||
'cohere.command-r-plus-v1:0': { prompt: 3.0, completion: 15.0 },
|
||||
'llama2-13b': { prompt: 0.75, completion: 1.0 },
|
||||
'llama2-70b': { prompt: 1.95, completion: 2.56 },
|
||||
'llama3-8b': { prompt: 0.3, completion: 0.6 },
|
||||
'llama3-70b': { prompt: 2.65, completion: 3.5 },
|
||||
'llama3-1-8b': { prompt: 0.3, completion: 0.6 },
|
||||
'llama3-1-70b': { prompt: 2.65, completion: 3.5 },
|
||||
'llama3-1-405b': { prompt: 5.32, completion: 16.0 },
|
||||
'mistral-7b': { prompt: 0.15, completion: 0.2 },
|
||||
'mistral-small': { prompt: 0.15, completion: 0.2 },
|
||||
'mixtral-8x7b': { prompt: 0.45, completion: 0.7 },
|
||||
'mistral-large-2402': { prompt: 4.0, completion: 12.0 },
|
||||
'mistral-large-2407': { prompt: 3.0, completion: 9.0 },
|
||||
'command-text': { prompt: 1.5, completion: 2.0 },
|
||||
'command-light': { prompt: 0.3, completion: 0.6 },
|
||||
'ai21.j2-mid-v1': { prompt: 12.5, completion: 12.5 },
|
||||
'ai21.j2-ultra-v1': { prompt: 18.8, completion: 18.8 },
|
||||
'ai21.jamba-instruct-v1:0': { prompt: 0.5, completion: 0.7 },
|
||||
'amazon.titan-text-lite-v1': { prompt: 0.15, completion: 0.2 },
|
||||
'amazon.titan-text-express-v1': { prompt: 0.2, completion: 0.6 },
|
||||
'amazon.titan-text-premier-v1:0': { prompt: 0.5, completion: 1.5 },
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(bedrockValues)) {
|
||||
bedrockValues[`bedrock/${key}`] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping of model token sizes to their respective multipliers for prompt and completion.
|
||||
* The rates are 1 USD per 1M tokens.
|
||||
@@ -59,6 +49,7 @@ const tokenValues = Object.assign(
|
||||
'claude-3-haiku': { prompt: 0.25, completion: 1.25 },
|
||||
'claude-2.1': { prompt: 8, completion: 24 },
|
||||
'claude-2': { prompt: 8, completion: 24 },
|
||||
'claude-instant': { prompt: 0.8, completion: 2.4 },
|
||||
'claude-': { prompt: 0.8, completion: 2.4 },
|
||||
'command-r-plus': { prompt: 3, completion: 15 },
|
||||
'command-r': { prompt: 0.5, completion: 1.5 },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const {
|
||||
defaultRate,
|
||||
tokenValues,
|
||||
@@ -224,34 +225,18 @@ describe('AWS Bedrock Model Tests', () => {
|
||||
|
||||
it('should return the correct prompt multipliers for all models', () => {
|
||||
const results = awsModels.map((model) => {
|
||||
const multiplier = getMultiplier({ valueKey: model, tokenType: 'prompt' });
|
||||
return multiplier === tokenValues[model].prompt;
|
||||
const valueKey = getValueKey(model, EModelEndpoint.bedrock);
|
||||
const multiplier = getMultiplier({ valueKey, tokenType: 'prompt' });
|
||||
return tokenValues[valueKey].prompt && multiplier === tokenValues[valueKey].prompt;
|
||||
});
|
||||
expect(results.every(Boolean)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return the correct completion multipliers for all models', () => {
|
||||
const results = awsModels.map((model) => {
|
||||
const multiplier = getMultiplier({ valueKey: model, tokenType: 'completion' });
|
||||
return multiplier === tokenValues[model].completion;
|
||||
});
|
||||
expect(results.every(Boolean)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return the correct prompt multipliers for all models with Bedrock prefix', () => {
|
||||
const results = awsModels.map((model) => {
|
||||
const modelName = `bedrock/${model}`;
|
||||
const multiplier = getMultiplier({ valueKey: modelName, tokenType: 'prompt' });
|
||||
return multiplier === tokenValues[model].prompt;
|
||||
});
|
||||
expect(results.every(Boolean)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return the correct completion multipliers for all models with Bedrock prefix', () => {
|
||||
const results = awsModels.map((model) => {
|
||||
const modelName = `bedrock/${model}`;
|
||||
const multiplier = getMultiplier({ valueKey: modelName, tokenType: 'completion' });
|
||||
return multiplier === tokenValues[model].completion;
|
||||
const valueKey = getValueKey(model, EModelEndpoint.bedrock);
|
||||
const multiplier = getMultiplier({ valueKey, tokenType: 'completion' });
|
||||
return tokenValues[valueKey].completion && multiplier === tokenValues[valueKey].completion;
|
||||
});
|
||||
expect(results.every(Boolean)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/backend",
|
||||
"version": "v0.7.5-rc1",
|
||||
"version": "v0.7.5-rc2",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "echo 'please run this from the root directory'",
|
||||
@@ -40,8 +40,10 @@
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@keyv/redis": "^2.8.1",
|
||||
"@langchain/community": "^0.0.46",
|
||||
"@langchain/core": "^0.2.18",
|
||||
"@langchain/google-genai": "^0.0.11",
|
||||
"@langchain/google-vertexai": "^0.0.17",
|
||||
"@librechat/agents": "^1.5.2",
|
||||
"axios": "^1.3.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
@@ -49,6 +51,7 @@
|
||||
"compression": "^1.7.4",
|
||||
"connect-redis": "^7.1.0",
|
||||
"cookie": "^0.5.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"dedent": "^1.5.3",
|
||||
"dotenv": "^16.0.3",
|
||||
|
||||
@@ -123,11 +123,6 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
};
|
||||
|
||||
let response = await client.sendMessage(text, messageOptions);
|
||||
|
||||
if (overrideParentMessageId) {
|
||||
response.parentMessageId = overrideParentMessageId;
|
||||
}
|
||||
|
||||
response.endpoint = endpointOption.endpoint;
|
||||
|
||||
const { conversation = {} } = await client.responsePromise;
|
||||
|
||||
@@ -44,6 +44,14 @@ async function endpointController(req, res) {
|
||||
};
|
||||
}
|
||||
|
||||
if (mergedConfig[EModelEndpoint.bedrock] && req.app.locals?.[EModelEndpoint.bedrock]) {
|
||||
const { availableRegions } = req.app.locals[EModelEndpoint.bedrock];
|
||||
mergedConfig[EModelEndpoint.bedrock] = {
|
||||
...mergedConfig[EModelEndpoint.bedrock],
|
||||
availableRegions,
|
||||
};
|
||||
}
|
||||
|
||||
const endpointsConfig = orderEndpointsConfig(mergedConfig);
|
||||
|
||||
await cache.set(CacheKeys.ENDPOINT_CONFIG, endpointsConfig);
|
||||
|
||||
@@ -2,6 +2,9 @@ const { CacheKeys } = require('librechat-data-provider');
|
||||
const { loadDefaultModels, loadConfigModels } = require('~/server/services/Config');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
/**
|
||||
* @param {ServerRequest} req
|
||||
*/
|
||||
const getModelsConfig = async (req) => {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
let modelsConfig = await cache.get(CacheKeys.MODELS_CONFIG);
|
||||
@@ -14,7 +17,7 @@ const getModelsConfig = async (req) => {
|
||||
|
||||
/**
|
||||
* Loads the models from the config.
|
||||
* @param {Express.Request} req - The Express request object.
|
||||
* @param {ServerRequest} req - The Express request object.
|
||||
* @returns {Promise<TModelsConfig>} The models config.
|
||||
*/
|
||||
async function loadModels(req) {
|
||||
|
||||
@@ -8,6 +8,7 @@ const {
|
||||
deleteMessages,
|
||||
deleteUserById,
|
||||
} = require('~/models');
|
||||
const User = require('~/models/User');
|
||||
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
|
||||
const { updateUserPluginsService, deleteUserKey } = require('~/server/services/UserService');
|
||||
const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService');
|
||||
@@ -20,6 +21,32 @@ const getUserController = async (req, res) => {
|
||||
res.status(200).send(req.user);
|
||||
};
|
||||
|
||||
const getTermsStatusController = async (req, res) => {
|
||||
try {
|
||||
const user = await User.findById(req.user.id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: 'User not found' });
|
||||
}
|
||||
res.status(200).json({ termsAccepted: !!user.termsAccepted });
|
||||
} catch (error) {
|
||||
logger.error('Error fetching terms acceptance status:', error);
|
||||
res.status(500).json({ message: 'Error fetching terms acceptance status' });
|
||||
}
|
||||
};
|
||||
|
||||
const acceptTermsController = async (req, res) => {
|
||||
try {
|
||||
const user = await User.findByIdAndUpdate(req.user.id, { termsAccepted: true }, { new: true });
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: 'User not found' });
|
||||
}
|
||||
res.status(200).json({ message: 'Terms accepted successfully' });
|
||||
} catch (error) {
|
||||
logger.error('Error accepting terms:', error);
|
||||
res.status(500).json({ message: 'Error accepting terms' });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteUserFiles = async (req) => {
|
||||
try {
|
||||
const userFiles = await getFiles({ user: req.user.id });
|
||||
@@ -135,6 +162,8 @@ const resendVerificationController = async (req, res) => {
|
||||
|
||||
module.exports = {
|
||||
getUserController,
|
||||
getTermsStatusController,
|
||||
acceptTermsController,
|
||||
deleteUserController,
|
||||
verifyEmailController,
|
||||
updateUserPluginsController,
|
||||
|
||||
127
api/server/controllers/agents/callbacks.js
Normal file
127
api/server/controllers/agents/callbacks.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const { GraphEvents, ToolEndHandler, ChatModelStreamHandler } = require('@librechat/agents');
|
||||
|
||||
/** @typedef {import('@librechat/agents').Graph} Graph */
|
||||
/** @typedef {import('@librechat/agents').EventHandler} EventHandler */
|
||||
/** @typedef {import('@librechat/agents').ModelEndData} ModelEndData */
|
||||
/** @typedef {import('@librechat/agents').ChatModelStreamHandler} ChatModelStreamHandler */
|
||||
/** @typedef {import('@librechat/agents').ContentAggregatorResult['aggregateContent']} ContentAggregator */
|
||||
/** @typedef {import('@librechat/agents').GraphEvents} GraphEvents */
|
||||
|
||||
/**
|
||||
* Sends message data in Server Sent Events format.
|
||||
* @param {ServerResponse} res - The server response.
|
||||
* @param {{ data: string | Record<string, unknown>, event?: string }} event - The message event.
|
||||
* @param {string} event.event - The type of event.
|
||||
* @param {string} event.data - The message to be sent.
|
||||
*/
|
||||
const sendEvent = (res, event) => {
|
||||
if (typeof event.data === 'string' && event.data.length === 0) {
|
||||
return;
|
||||
}
|
||||
res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`);
|
||||
};
|
||||
|
||||
class ModelEndHandler {
|
||||
/**
|
||||
* @param {Array<UsageMetadata>} collectedUsage
|
||||
*/
|
||||
constructor(collectedUsage) {
|
||||
if (!Array.isArray(collectedUsage)) {
|
||||
throw new Error('collectedUsage must be an array');
|
||||
}
|
||||
this.collectedUsage = collectedUsage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} event
|
||||
* @param {ModelEndData | undefined} data
|
||||
* @param {Record<string, unknown> | undefined} metadata
|
||||
* @param {Graph} graph
|
||||
* @returns
|
||||
*/
|
||||
handle(event, data, metadata, graph) {
|
||||
if (!graph || !metadata) {
|
||||
console.warn(`Graph or metadata not found in ${event} event`);
|
||||
return;
|
||||
}
|
||||
|
||||
const usage = data?.output?.usage_metadata;
|
||||
|
||||
if (usage) {
|
||||
this.collectedUsage.push(usage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default handlers for stream events.
|
||||
* @param {Object} options - The options object.
|
||||
* @param {ServerResponse} options.res - The options object.
|
||||
* @param {ContentAggregator} options.aggregateContent - The options object.
|
||||
* @param {Array<UsageMetadata>} options.collectedUsage - The list of collected usage metadata.
|
||||
* @returns {Record<string, t.EventHandler>} The default handlers.
|
||||
* @throws {Error} If the request is not found.
|
||||
*/
|
||||
function getDefaultHandlers({ res, aggregateContent, collectedUsage }) {
|
||||
if (!res || !aggregateContent) {
|
||||
throw new Error(
|
||||
`[getDefaultHandlers] Missing required options: res: ${!res}, aggregateContent: ${!aggregateContent}`,
|
||||
);
|
||||
}
|
||||
const handlers = {
|
||||
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
|
||||
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
||||
[GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
|
||||
[GraphEvents.ON_RUN_STEP]: {
|
||||
/**
|
||||
* Handle ON_RUN_STEP event.
|
||||
* @param {string} event - The event name.
|
||||
* @param {StreamEventData} data - The event data.
|
||||
*/
|
||||
handle: (event, data) => {
|
||||
sendEvent(res, { event, data });
|
||||
aggregateContent({ event, data });
|
||||
},
|
||||
},
|
||||
[GraphEvents.ON_RUN_STEP_DELTA]: {
|
||||
/**
|
||||
* Handle ON_RUN_STEP_DELTA event.
|
||||
* @param {string} event - The event name.
|
||||
* @param {StreamEventData} data - The event data.
|
||||
*/
|
||||
handle: (event, data) => {
|
||||
sendEvent(res, { event, data });
|
||||
aggregateContent({ event, data });
|
||||
},
|
||||
},
|
||||
[GraphEvents.ON_RUN_STEP_COMPLETED]: {
|
||||
/**
|
||||
* Handle ON_RUN_STEP_COMPLETED event.
|
||||
* @param {string} event - The event name.
|
||||
* @param {StreamEventData & { result: ToolEndData }} data - The event data.
|
||||
*/
|
||||
handle: (event, data) => {
|
||||
sendEvent(res, { event, data });
|
||||
aggregateContent({ event, data });
|
||||
},
|
||||
},
|
||||
[GraphEvents.ON_MESSAGE_DELTA]: {
|
||||
/**
|
||||
* Handle ON_MESSAGE_DELTA event.
|
||||
* @param {string} event - The event name.
|
||||
* @param {StreamEventData} data - The event data.
|
||||
*/
|
||||
handle: (event, data) => {
|
||||
sendEvent(res, { event, data });
|
||||
aggregateContent({ event, data });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendEvent,
|
||||
getDefaultHandlers,
|
||||
};
|
||||
608
api/server/controllers/agents/client.js
Normal file
608
api/server/controllers/agents/client.js
Normal file
@@ -0,0 +1,608 @@
|
||||
// const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
// const {
|
||||
// Constants,
|
||||
// ImageDetail,
|
||||
// EModelEndpoint,
|
||||
// resolveHeaders,
|
||||
// validateVisionModel,
|
||||
// mapModelToAzureConfig,
|
||||
// } = require('librechat-data-provider');
|
||||
const { Callback, createMetadataAggregator } = require('@librechat/agents');
|
||||
const {
|
||||
Constants,
|
||||
EModelEndpoint,
|
||||
bedrockOutputParser,
|
||||
providerEndpointMap,
|
||||
removeNullishValues,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
extractBaseURL,
|
||||
// constructAzureURL,
|
||||
// genAzureChatCompletion,
|
||||
} = require('~/utils');
|
||||
const {
|
||||
formatMessage,
|
||||
formatAgentMessages,
|
||||
createContextHandlers,
|
||||
} = require('~/app/clients/prompts');
|
||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||
const Tokenizer = require('~/server/services/Tokenizer');
|
||||
const { spendTokens } = require('~/models/spendTokens');
|
||||
const BaseClient = require('~/app/clients/BaseClient');
|
||||
// const { sleep } = require('~/server/utils');
|
||||
const { createRun } = require('./run');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/** @typedef {import('@librechat/agents').MessageContentComplex} MessageContentComplex */
|
||||
|
||||
// const providerSchemas = {
|
||||
// [EModelEndpoint.bedrock]: true,
|
||||
// };
|
||||
|
||||
const providerParsers = {
|
||||
[EModelEndpoint.bedrock]: bedrockOutputParser,
|
||||
};
|
||||
|
||||
class AgentClient extends BaseClient {
|
||||
constructor(options = {}) {
|
||||
super(null, options);
|
||||
|
||||
/** @type {'discard' | 'summarize'} */
|
||||
this.contextStrategy = 'discard';
|
||||
|
||||
/** @deprecated @type {true} - Is a Chat Completion Request */
|
||||
this.isChatCompletion = true;
|
||||
|
||||
/** @type {AgentRun} */
|
||||
this.run;
|
||||
|
||||
const {
|
||||
maxContextTokens,
|
||||
modelOptions = {},
|
||||
contentParts,
|
||||
collectedUsage,
|
||||
...clientOptions
|
||||
} = options;
|
||||
|
||||
this.modelOptions = modelOptions;
|
||||
this.maxContextTokens = maxContextTokens;
|
||||
/** @type {MessageContentComplex[]} */
|
||||
this.contentParts = contentParts;
|
||||
/** @type {Array<UsageMetadata>} */
|
||||
this.collectedUsage = collectedUsage;
|
||||
this.options = Object.assign({ endpoint: options.endpoint }, clientOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the aggregated content parts for the current run.
|
||||
* @returns {MessageContentComplex[]} */
|
||||
getContentParts() {
|
||||
return this.contentParts;
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
logger.info('[api/server/controllers/agents/client.js] setOptions', options);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Checks if the model is a vision model based on request attachments and sets the appropriate options:
|
||||
* - Sets `this.modelOptions.model` to `gpt-4-vision-preview` if the request is a vision request.
|
||||
* - Sets `this.isVisionModel` to `true` if vision request.
|
||||
* - Deletes `this.modelOptions.stop` if vision request.
|
||||
* @param {MongoFile[]} attachments
|
||||
*/
|
||||
checkVisionRequest(attachments) {
|
||||
logger.info(
|
||||
'[api/server/controllers/agents/client.js #checkVisionRequest] not implemented',
|
||||
attachments,
|
||||
);
|
||||
// if (!attachments) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const availableModels = this.options.modelsConfig?.[this.options.endpoint];
|
||||
// if (!availableModels) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let visionRequestDetected = false;
|
||||
// for (const file of attachments) {
|
||||
// if (file?.type?.includes('image')) {
|
||||
// visionRequestDetected = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// if (!visionRequestDetected) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels });
|
||||
// if (this.isVisionModel) {
|
||||
// delete this.modelOptions.stop;
|
||||
// return;
|
||||
// }
|
||||
|
||||
// for (const model of availableModels) {
|
||||
// if (!validateVisionModel({ model, availableModels })) {
|
||||
// continue;
|
||||
// }
|
||||
// this.modelOptions.model = model;
|
||||
// this.isVisionModel = true;
|
||||
// delete this.modelOptions.stop;
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!availableModels.includes(this.defaultVisionModel)) {
|
||||
// return;
|
||||
// }
|
||||
// if (!validateVisionModel({ model: this.defaultVisionModel, availableModels })) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.modelOptions.model = this.defaultVisionModel;
|
||||
// this.isVisionModel = true;
|
||||
// delete this.modelOptions.stop;
|
||||
}
|
||||
|
||||
getSaveOptions() {
|
||||
const parseOptions = providerParsers[this.options.endpoint];
|
||||
let runOptions =
|
||||
this.options.endpoint === EModelEndpoint.agents
|
||||
? {
|
||||
model: undefined,
|
||||
// TODO:
|
||||
// would need to be override settings; otherwise, model needs to be undefined
|
||||
// model: this.override.model,
|
||||
// instructions: this.override.instructions,
|
||||
// additional_instructions: this.override.additional_instructions,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (parseOptions) {
|
||||
runOptions = parseOptions(this.modelOptions);
|
||||
}
|
||||
|
||||
return removeNullishValues(
|
||||
Object.assign(
|
||||
{
|
||||
endpoint: this.options.endpoint,
|
||||
agent_id: this.options.agent.id,
|
||||
modelLabel: this.options.modelLabel,
|
||||
maxContextTokens: this.options.maxContextTokens,
|
||||
resendFiles: this.options.resendFiles,
|
||||
imageDetail: this.options.imageDetail,
|
||||
spec: this.options.spec,
|
||||
},
|
||||
// TODO: PARSE OPTIONS BY PROVIDER, MAY CONTAIN SENSITIVE DATA
|
||||
runOptions,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getBuildMessagesOptions(opts) {
|
||||
return {
|
||||
instructions: opts.instructions,
|
||||
additional_instructions: opts.additional_instructions,
|
||||
};
|
||||
}
|
||||
|
||||
async addImageURLs(message, attachments) {
|
||||
const { files, image_urls } = await encodeAndFormat(
|
||||
this.options.req,
|
||||
attachments,
|
||||
this.options.agent.provider,
|
||||
);
|
||||
message.image_urls = image_urls.length ? image_urls : undefined;
|
||||
return files;
|
||||
}
|
||||
|
||||
async buildMessages(
|
||||
messages,
|
||||
parentMessageId,
|
||||
{ instructions = null, additional_instructions = null },
|
||||
opts,
|
||||
) {
|
||||
let orderedMessages = this.constructor.getMessagesForConversation({
|
||||
messages,
|
||||
parentMessageId,
|
||||
summary: this.shouldSummarize,
|
||||
});
|
||||
|
||||
let payload;
|
||||
/** @type {{ role: string; name: string; content: string } | undefined} */
|
||||
let systemMessage;
|
||||
/** @type {number | undefined} */
|
||||
let promptTokens;
|
||||
|
||||
/** @type {string} */
|
||||
let systemContent = `${instructions ?? ''}${additional_instructions ?? ''}`;
|
||||
|
||||
if (this.options.attachments) {
|
||||
const attachments = await this.options.attachments;
|
||||
|
||||
if (this.message_file_map) {
|
||||
this.message_file_map[orderedMessages[orderedMessages.length - 1].messageId] = attachments;
|
||||
} else {
|
||||
this.message_file_map = {
|
||||
[orderedMessages[orderedMessages.length - 1].messageId]: attachments,
|
||||
};
|
||||
}
|
||||
|
||||
const files = await this.addImageURLs(
|
||||
orderedMessages[orderedMessages.length - 1],
|
||||
attachments,
|
||||
);
|
||||
|
||||
this.options.attachments = files;
|
||||
}
|
||||
|
||||
if (this.message_file_map) {
|
||||
this.contextHandlers = createContextHandlers(
|
||||
this.options.req,
|
||||
orderedMessages[orderedMessages.length - 1].text,
|
||||
);
|
||||
}
|
||||
|
||||
const formattedMessages = orderedMessages.map((message, i) => {
|
||||
const formattedMessage = formatMessage({
|
||||
message,
|
||||
userName: this.options?.name,
|
||||
assistantName: this.options?.modelLabel,
|
||||
});
|
||||
|
||||
const needsTokenCount = this.contextStrategy && !orderedMessages[i].tokenCount;
|
||||
|
||||
/* If tokens were never counted, or, is a Vision request and the message has files, count again */
|
||||
if (needsTokenCount || (this.isVisionModel && (message.image_urls || message.files))) {
|
||||
orderedMessages[i].tokenCount = this.getTokenCountForMessage(formattedMessage);
|
||||
}
|
||||
|
||||
/* If message has files, calculate image token cost */
|
||||
// if (this.message_file_map && this.message_file_map[message.messageId]) {
|
||||
// const attachments = this.message_file_map[message.messageId];
|
||||
// for (const file of attachments) {
|
||||
// if (file.embedded) {
|
||||
// this.contextHandlers?.processFile(file);
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// orderedMessages[i].tokenCount += this.calculateImageTokenCost({
|
||||
// width: file.width,
|
||||
// height: file.height,
|
||||
// detail: this.options.imageDetail ?? ImageDetail.auto,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
return formattedMessage;
|
||||
});
|
||||
|
||||
if (this.contextHandlers) {
|
||||
this.augmentedPrompt = await this.contextHandlers.createContext();
|
||||
systemContent = this.augmentedPrompt + systemContent;
|
||||
}
|
||||
|
||||
if (systemContent) {
|
||||
systemContent = `${systemContent.trim()}`;
|
||||
systemMessage = {
|
||||
role: 'system',
|
||||
name: 'instructions',
|
||||
content: systemContent,
|
||||
};
|
||||
|
||||
if (this.contextStrategy) {
|
||||
const instructionTokens = this.getTokenCountForMessage(systemMessage);
|
||||
if (instructionTokens >= 0) {
|
||||
const firstMessageTokens = orderedMessages[0].tokenCount ?? 0;
|
||||
orderedMessages[0].tokenCount = firstMessageTokens + instructionTokens;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.contextStrategy) {
|
||||
({ payload, promptTokens, messages } = await this.handleContextStrategy({
|
||||
orderedMessages,
|
||||
formattedMessages,
|
||||
/* prefer usage_metadata from final message */
|
||||
buildTokenMap: false,
|
||||
}));
|
||||
}
|
||||
|
||||
const result = {
|
||||
prompt: payload,
|
||||
promptTokens,
|
||||
messages,
|
||||
};
|
||||
|
||||
if (promptTokens >= 0 && typeof opts?.getReqData === 'function') {
|
||||
opts.getReqData({ promptTokens });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @type {sendCompletion} */
|
||||
async sendCompletion(payload, opts = {}) {
|
||||
this.modelOptions.user = this.user;
|
||||
await this.chatCompletion({
|
||||
payload,
|
||||
onProgress: opts.onProgress,
|
||||
abortController: opts.abortController,
|
||||
});
|
||||
return this.contentParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {string} [params.model]
|
||||
* @param {string} [params.context='message']
|
||||
* @param {UsageMetadata[]} [params.collectedUsage=this.collectedUsage]
|
||||
*/
|
||||
async recordCollectedUsage({ model, context = 'message', collectedUsage = this.collectedUsage }) {
|
||||
for (const usage of collectedUsage) {
|
||||
await spendTokens(
|
||||
{
|
||||
context,
|
||||
model: model ?? this.modelOptions.model,
|
||||
conversationId: this.conversationId,
|
||||
user: this.user ?? this.options.req.user?.id,
|
||||
endpointTokenConfig: this.options.endpointTokenConfig,
|
||||
},
|
||||
{ promptTokens: usage.input_tokens, completionTokens: usage.output_tokens },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async chatCompletion({ payload, abortController = null }) {
|
||||
try {
|
||||
if (!abortController) {
|
||||
abortController = new AbortController();
|
||||
}
|
||||
|
||||
const baseURL = extractBaseURL(this.completionsUrl);
|
||||
logger.debug('[api/server/controllers/agents/client.js] chatCompletion', {
|
||||
baseURL,
|
||||
payload,
|
||||
});
|
||||
|
||||
// if (this.useOpenRouter) {
|
||||
// opts.defaultHeaders = {
|
||||
// 'HTTP-Referer': 'https://librechat.ai',
|
||||
// 'X-Title': 'LibreChat',
|
||||
// };
|
||||
// }
|
||||
|
||||
// if (this.options.headers) {
|
||||
// opts.defaultHeaders = { ...opts.defaultHeaders, ...this.options.headers };
|
||||
// }
|
||||
|
||||
// if (this.options.proxy) {
|
||||
// opts.httpAgent = new HttpsProxyAgent(this.options.proxy);
|
||||
// }
|
||||
|
||||
// if (this.isVisionModel) {
|
||||
// modelOptions.max_tokens = 4000;
|
||||
// }
|
||||
|
||||
// /** @type {TAzureConfig | undefined} */
|
||||
// const azureConfig = this.options?.req?.app?.locals?.[EModelEndpoint.azureOpenAI];
|
||||
|
||||
// if (
|
||||
// (this.azure && this.isVisionModel && azureConfig) ||
|
||||
// (azureConfig && this.isVisionModel && this.options.endpoint === EModelEndpoint.azureOpenAI)
|
||||
// ) {
|
||||
// const { modelGroupMap, groupMap } = azureConfig;
|
||||
// const {
|
||||
// azureOptions,
|
||||
// baseURL,
|
||||
// headers = {},
|
||||
// serverless,
|
||||
// } = mapModelToAzureConfig({
|
||||
// modelName: modelOptions.model,
|
||||
// modelGroupMap,
|
||||
// groupMap,
|
||||
// });
|
||||
// opts.defaultHeaders = resolveHeaders(headers);
|
||||
// this.langchainProxy = extractBaseURL(baseURL);
|
||||
// this.apiKey = azureOptions.azureOpenAIApiKey;
|
||||
|
||||
// const groupName = modelGroupMap[modelOptions.model].group;
|
||||
// this.options.addParams = azureConfig.groupMap[groupName].addParams;
|
||||
// this.options.dropParams = azureConfig.groupMap[groupName].dropParams;
|
||||
// // Note: `forcePrompt` not re-assigned as only chat models are vision models
|
||||
|
||||
// this.azure = !serverless && azureOptions;
|
||||
// this.azureEndpoint =
|
||||
// !serverless && genAzureChatCompletion(this.azure, modelOptions.model, this);
|
||||
// }
|
||||
|
||||
// if (this.azure || this.options.azure) {
|
||||
// /* Azure Bug, extremely short default `max_tokens` response */
|
||||
// if (!modelOptions.max_tokens && modelOptions.model === 'gpt-4-vision-preview') {
|
||||
// modelOptions.max_tokens = 4000;
|
||||
// }
|
||||
|
||||
// /* Azure does not accept `model` in the body, so we need to remove it. */
|
||||
// delete modelOptions.model;
|
||||
|
||||
// opts.baseURL = this.langchainProxy
|
||||
// ? constructAzureURL({
|
||||
// baseURL: this.langchainProxy,
|
||||
// azureOptions: this.azure,
|
||||
// })
|
||||
// : this.azureEndpoint.split(/(?<!\/)\/(chat|completion)\//)[0];
|
||||
|
||||
// opts.defaultQuery = { 'api-version': this.azure.azureOpenAIApiVersion };
|
||||
// opts.defaultHeaders = { ...opts.defaultHeaders, 'api-key': this.apiKey };
|
||||
// }
|
||||
|
||||
// if (process.env.OPENAI_ORGANIZATION) {
|
||||
// opts.organization = process.env.OPENAI_ORGANIZATION;
|
||||
// }
|
||||
|
||||
// if (this.options.addParams && typeof this.options.addParams === 'object') {
|
||||
// modelOptions = {
|
||||
// ...modelOptions,
|
||||
// ...this.options.addParams,
|
||||
// };
|
||||
// logger.debug('[api/server/controllers/agents/client.js #chatCompletion] added params', {
|
||||
// addParams: this.options.addParams,
|
||||
// modelOptions,
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (this.options.dropParams && Array.isArray(this.options.dropParams)) {
|
||||
// this.options.dropParams.forEach((param) => {
|
||||
// delete modelOptions[param];
|
||||
// });
|
||||
// logger.debug('[api/server/controllers/agents/client.js #chatCompletion] dropped params', {
|
||||
// dropParams: this.options.dropParams,
|
||||
// modelOptions,
|
||||
// });
|
||||
// }
|
||||
|
||||
const run = await createRun({
|
||||
req: this.options.req,
|
||||
agent: this.options.agent,
|
||||
tools: this.options.tools,
|
||||
toolMap: this.options.toolMap,
|
||||
runId: this.responseMessageId,
|
||||
modelOptions: this.modelOptions,
|
||||
customHandlers: this.options.eventHandlers,
|
||||
});
|
||||
|
||||
const config = {
|
||||
configurable: {
|
||||
provider: providerEndpointMap[this.options.agent.provider],
|
||||
thread_id: this.conversationId,
|
||||
},
|
||||
run_id: this.responseMessageId,
|
||||
signal: abortController.signal,
|
||||
streamMode: 'values',
|
||||
version: 'v2',
|
||||
};
|
||||
|
||||
if (!run) {
|
||||
throw new Error('Failed to create run');
|
||||
}
|
||||
|
||||
this.run = run;
|
||||
|
||||
const messages = formatAgentMessages(payload);
|
||||
await run.processStream({ messages }, config, {
|
||||
[Callback.TOOL_ERROR]: (graph, error, toolId) => {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #chatCompletion] Tool Error',
|
||||
error,
|
||||
toolId,
|
||||
);
|
||||
},
|
||||
});
|
||||
this.recordCollectedUsage({ context: 'message' }).catch((err) => {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #chatCompletion] Error recording collected usage',
|
||||
err,
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #sendCompletion] Unhandled error type',
|
||||
err,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'[api/server/controllers/agents/client.js #sendCompletion] Operation aborted',
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {string} params.text
|
||||
* @param {string} params.conversationId
|
||||
*/
|
||||
async titleConvo({ text }) {
|
||||
if (!this.run) {
|
||||
throw new Error('Run not initialized');
|
||||
}
|
||||
const { handleLLMEnd, collected: collectedMetadata } = createMetadataAggregator();
|
||||
const clientOptions = {};
|
||||
const providerConfig = this.options.req.app.locals[this.options.agent.provider];
|
||||
if (
|
||||
providerConfig &&
|
||||
providerConfig.titleModel &&
|
||||
providerConfig.titleModel !== Constants.CURRENT_MODEL
|
||||
) {
|
||||
clientOptions.model = providerConfig.titleModel;
|
||||
}
|
||||
try {
|
||||
const titleResult = await this.run.generateTitle({
|
||||
inputText: text,
|
||||
contentParts: this.contentParts,
|
||||
clientOptions,
|
||||
chainOptions: {
|
||||
callbacks: [
|
||||
{
|
||||
handleLLMEnd,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const collectedUsage = collectedMetadata.map((item) => {
|
||||
let input_tokens, output_tokens;
|
||||
|
||||
if (item.usage) {
|
||||
input_tokens = item.usage.input_tokens || item.usage.inputTokens;
|
||||
output_tokens = item.usage.output_tokens || item.usage.outputTokens;
|
||||
} else if (item.tokenUsage) {
|
||||
input_tokens = item.tokenUsage.promptTokens;
|
||||
output_tokens = item.tokenUsage.completionTokens;
|
||||
}
|
||||
|
||||
return {
|
||||
input_tokens: input_tokens,
|
||||
output_tokens: output_tokens,
|
||||
};
|
||||
});
|
||||
|
||||
this.recordCollectedUsage({
|
||||
model: clientOptions.model,
|
||||
context: 'title',
|
||||
collectedUsage,
|
||||
}).catch((err) => {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #titleConvo] Error recording collected usage',
|
||||
err,
|
||||
);
|
||||
});
|
||||
|
||||
return titleResult.title;
|
||||
} catch (err) {
|
||||
logger.error('[api/server/controllers/agents/client.js #titleConvo] Error', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
getEncoding() {
|
||||
return this.modelOptions.model?.includes('gpt-4o') ? 'o200k_base' : 'cl100k_base';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the token count of a given text. It also checks and resets the tokenizers if necessary.
|
||||
* @param {string} text - The text to get the token count for.
|
||||
* @returns {number} The token count of the given text.
|
||||
*/
|
||||
getTokenCount(text) {
|
||||
const encoding = this.getEncoding();
|
||||
return Tokenizer.getTokenCount(text, encoding);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AgentClient;
|
||||
153
api/server/controllers/agents/errors.js
Normal file
153
api/server/controllers/agents/errors.js
Normal file
@@ -0,0 +1,153 @@
|
||||
// errorHandler.js
|
||||
const { logger } = require('~/config');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { CacheKeys, ViolationTypes } = require('librechat-data-provider');
|
||||
const { recordUsage } = require('~/server/services/Threads');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
const { sendResponse } = require('~/server/utils');
|
||||
|
||||
/**
|
||||
* @typedef {Object} ErrorHandlerContext
|
||||
* @property {OpenAIClient} openai - The OpenAI client
|
||||
* @property {string} run_id - The run ID
|
||||
* @property {boolean} completedRun - Whether the run has completed
|
||||
* @property {string} assistant_id - The assistant ID
|
||||
* @property {string} conversationId - The conversation ID
|
||||
* @property {string} parentMessageId - The parent message ID
|
||||
* @property {string} responseMessageId - The response message ID
|
||||
* @property {string} endpoint - The endpoint being used
|
||||
* @property {string} cacheKey - The cache key for the current request
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ErrorHandlerDependencies
|
||||
* @property {Express.Request} req - The Express request object
|
||||
* @property {Express.Response} res - The Express response object
|
||||
* @property {() => ErrorHandlerContext} getContext - Function to get the current context
|
||||
* @property {string} [originPath] - The origin path for the error handler
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates an error handler function with the given dependencies
|
||||
* @param {ErrorHandlerDependencies} dependencies - The dependencies for the error handler
|
||||
* @returns {(error: Error) => Promise<void>} The error handler function
|
||||
*/
|
||||
const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/chat/' }) => {
|
||||
const cache = getLogStores(CacheKeys.ABORT_KEYS);
|
||||
|
||||
/**
|
||||
* Handles errors that occur during the chat process
|
||||
* @param {Error} error - The error that occurred
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
return async (error) => {
|
||||
const {
|
||||
openai,
|
||||
run_id,
|
||||
endpoint,
|
||||
cacheKey,
|
||||
completedRun,
|
||||
assistant_id,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
responseMessageId,
|
||||
} = getContext();
|
||||
|
||||
const defaultErrorMessage =
|
||||
'The Assistant run failed to initialize. Try sending a message in a new conversation.';
|
||||
const messageData = {
|
||||
assistant_id,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender: 'System',
|
||||
user: req.user.id,
|
||||
shouldSaveMessage: false,
|
||||
messageId: responseMessageId,
|
||||
endpoint,
|
||||
};
|
||||
|
||||
if (error.message === 'Run cancelled') {
|
||||
return res.end();
|
||||
} else if (error.message === 'Request closed' && completedRun) {
|
||||
return;
|
||||
} else if (error.message === 'Request closed') {
|
||||
logger.debug(`[${originPath}] Request aborted on close`);
|
||||
} else if (/Files.*are invalid/.test(error.message)) {
|
||||
const errorMessage = `Files are invalid, or may not have uploaded yet.${
|
||||
endpoint === 'azureAssistants'
|
||||
? ' If using Azure OpenAI, files are only available in the region of the assistant\'s model at the time of upload.'
|
||||
: ''
|
||||
}`;
|
||||
return sendResponse(req, res, messageData, errorMessage);
|
||||
} else if (error?.message?.includes('string too long')) {
|
||||
return sendResponse(
|
||||
req,
|
||||
res,
|
||||
messageData,
|
||||
'Message too long. The Assistants API has a limit of 32,768 characters per message. Please shorten it and try again.',
|
||||
);
|
||||
} else if (error?.message?.includes(ViolationTypes.TOKEN_BALANCE)) {
|
||||
return sendResponse(req, res, messageData, error.message);
|
||||
} else {
|
||||
logger.error(`[${originPath}]`, error);
|
||||
}
|
||||
|
||||
if (!openai || !run_id) {
|
||||
return sendResponse(req, res, messageData, defaultErrorMessage);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
try {
|
||||
const status = await cache.get(cacheKey);
|
||||
if (status === 'cancelled') {
|
||||
logger.debug(`[${originPath}] Run already cancelled`);
|
||||
return res.end();
|
||||
}
|
||||
await cache.delete(cacheKey);
|
||||
// const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id);
|
||||
// logger.debug(`[${originPath}] Cancelled run:`, cancelledRun);
|
||||
} catch (error) {
|
||||
logger.error(`[${originPath}] Error cancelling run`, error);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
let run;
|
||||
try {
|
||||
// run = await openai.beta.threads.runs.retrieve(thread_id, run_id);
|
||||
await recordUsage({
|
||||
...run.usage,
|
||||
model: run.model,
|
||||
user: req.user.id,
|
||||
conversationId,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`[${originPath}] Error fetching or processing run`, error);
|
||||
}
|
||||
|
||||
let finalEvent;
|
||||
try {
|
||||
// const errorContentPart = {
|
||||
// text: {
|
||||
// value:
|
||||
// error?.message ?? 'There was an error processing your request. Please try again later.',
|
||||
// },
|
||||
// type: ContentTypes.ERROR,
|
||||
// };
|
||||
|
||||
finalEvent = {
|
||||
final: true,
|
||||
conversation: await getConvo(req.user.id, conversationId),
|
||||
// runMessages,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`[${originPath}] Error finalizing error process`, error);
|
||||
return sendResponse(req, res, messageData, 'The Assistant run failed');
|
||||
}
|
||||
|
||||
return sendResponse(req, res, finalEvent);
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { createErrorHandler };
|
||||
106
api/server/controllers/agents/llm.js
Normal file
106
api/server/controllers/agents/llm.js
Normal file
@@ -0,0 +1,106 @@
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { resolveHeaders } = require('librechat-data-provider');
|
||||
const { createLLM } = require('~/app/clients/llm');
|
||||
|
||||
/**
|
||||
* Initializes and returns a Language Learning Model (LLM) instance.
|
||||
*
|
||||
* @param {Object} options - Configuration options for the LLM.
|
||||
* @param {string} options.model - The model identifier.
|
||||
* @param {string} options.modelName - The specific name of the model.
|
||||
* @param {number} options.temperature - The temperature setting for the model.
|
||||
* @param {number} options.presence_penalty - The presence penalty for the model.
|
||||
* @param {number} options.frequency_penalty - The frequency penalty for the model.
|
||||
* @param {number} options.max_tokens - The maximum number of tokens for the model output.
|
||||
* @param {boolean} options.streaming - Whether to use streaming for the model output.
|
||||
* @param {Object} options.context - The context for the conversation.
|
||||
* @param {number} options.tokenBuffer - The token buffer size.
|
||||
* @param {number} options.initialMessageCount - The initial message count.
|
||||
* @param {string} options.conversationId - The ID of the conversation.
|
||||
* @param {string} options.user - The user identifier.
|
||||
* @param {string} options.langchainProxy - The langchain proxy URL.
|
||||
* @param {boolean} options.useOpenRouter - Whether to use OpenRouter.
|
||||
* @param {Object} options.options - Additional options.
|
||||
* @param {Object} options.options.headers - Custom headers for the request.
|
||||
* @param {string} options.options.proxy - Proxy URL.
|
||||
* @param {Object} options.options.req - The request object.
|
||||
* @param {Object} options.options.res - The response object.
|
||||
* @param {boolean} options.options.debug - Whether to enable debug mode.
|
||||
* @param {string} options.apiKey - The API key for authentication.
|
||||
* @param {Object} options.azure - Azure-specific configuration.
|
||||
* @param {Object} options.abortController - The AbortController instance.
|
||||
* @returns {Object} The initialized LLM instance.
|
||||
*/
|
||||
function initializeLLM(options) {
|
||||
const {
|
||||
model,
|
||||
modelName,
|
||||
temperature,
|
||||
presence_penalty,
|
||||
frequency_penalty,
|
||||
max_tokens,
|
||||
streaming,
|
||||
user,
|
||||
langchainProxy,
|
||||
useOpenRouter,
|
||||
options: { headers, proxy },
|
||||
apiKey,
|
||||
azure,
|
||||
} = options;
|
||||
|
||||
const modelOptions = {
|
||||
modelName: modelName || model,
|
||||
temperature,
|
||||
presence_penalty,
|
||||
frequency_penalty,
|
||||
user,
|
||||
};
|
||||
|
||||
if (max_tokens) {
|
||||
modelOptions.max_tokens = max_tokens;
|
||||
}
|
||||
|
||||
const configOptions = {};
|
||||
|
||||
if (langchainProxy) {
|
||||
configOptions.basePath = langchainProxy;
|
||||
}
|
||||
|
||||
if (useOpenRouter) {
|
||||
configOptions.basePath = 'https://openrouter.ai/api/v1';
|
||||
configOptions.baseOptions = {
|
||||
headers: {
|
||||
'HTTP-Referer': 'https://librechat.ai',
|
||||
'X-Title': 'LibreChat',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
|
||||
configOptions.baseOptions = {
|
||||
headers: resolveHeaders({
|
||||
...headers,
|
||||
...configOptions?.baseOptions?.headers,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (proxy) {
|
||||
configOptions.httpAgent = new HttpsProxyAgent(proxy);
|
||||
configOptions.httpsAgent = new HttpsProxyAgent(proxy);
|
||||
}
|
||||
|
||||
const llm = createLLM({
|
||||
modelOptions,
|
||||
configOptions,
|
||||
openAIApiKey: apiKey,
|
||||
azure,
|
||||
streaming,
|
||||
});
|
||||
|
||||
return llm;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeLLM,
|
||||
};
|
||||
142
api/server/controllers/agents/request.js
Normal file
142
api/server/controllers/agents/request.js
Normal file
@@ -0,0 +1,142 @@
|
||||
const { Constants } = require('librechat-data-provider');
|
||||
const { createAbortController, handleAbortError } = require('~/server/middleware');
|
||||
const { sendMessage } = require('~/server/utils');
|
||||
const { saveMessage } = require('~/models');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||
let {
|
||||
text,
|
||||
endpointOption,
|
||||
conversationId,
|
||||
parentMessageId = null,
|
||||
overrideParentMessageId = null,
|
||||
} = req.body;
|
||||
|
||||
let sender;
|
||||
let userMessage;
|
||||
let promptTokens;
|
||||
let userMessageId;
|
||||
let responseMessageId;
|
||||
let userMessagePromise;
|
||||
|
||||
const newConvo = !conversationId;
|
||||
const user = req.user.id;
|
||||
|
||||
const getReqData = (data = {}) => {
|
||||
for (let key in data) {
|
||||
if (key === 'userMessage') {
|
||||
userMessage = data[key];
|
||||
userMessageId = data[key].messageId;
|
||||
} else if (key === 'userMessagePromise') {
|
||||
userMessagePromise = data[key];
|
||||
} else if (key === 'responseMessageId') {
|
||||
responseMessageId = data[key];
|
||||
} else if (key === 'promptTokens') {
|
||||
promptTokens = data[key];
|
||||
} else if (key === 'sender') {
|
||||
sender = data[key];
|
||||
} else if (!conversationId && key === 'conversationId') {
|
||||
conversationId = data[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
/** @type {{ client: TAgentClient }} */
|
||||
const { client } = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
const getAbortData = () => ({
|
||||
sender,
|
||||
userMessage,
|
||||
promptTokens,
|
||||
conversationId,
|
||||
userMessagePromise,
|
||||
messageId: responseMessageId,
|
||||
content: client.getContentParts(),
|
||||
parentMessageId: overrideParentMessageId ?? userMessageId,
|
||||
});
|
||||
|
||||
const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData);
|
||||
|
||||
res.on('close', () => {
|
||||
logger.debug('[AgentController] Request closed');
|
||||
if (!abortController) {
|
||||
return;
|
||||
} else if (abortController.signal.aborted) {
|
||||
return;
|
||||
} else if (abortController.requestCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
abortController.abort();
|
||||
logger.debug('[AgentController] Request aborted on close');
|
||||
});
|
||||
|
||||
const messageOptions = {
|
||||
user,
|
||||
onStart,
|
||||
getReqData,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
abortController,
|
||||
overrideParentMessageId,
|
||||
progressOptions: {
|
||||
res,
|
||||
// parentMessageId: overrideParentMessageId || userMessageId,
|
||||
},
|
||||
};
|
||||
|
||||
let response = await client.sendMessage(text, messageOptions);
|
||||
response.endpoint = endpointOption.endpoint;
|
||||
|
||||
const { conversation = {} } = await client.responsePromise;
|
||||
conversation.title =
|
||||
conversation && !conversation.title ? null : conversation?.title || 'New Chat';
|
||||
|
||||
if (client.options.attachments) {
|
||||
userMessage.files = client.options.attachments;
|
||||
delete userMessage.image_urls;
|
||||
}
|
||||
|
||||
if (!abortController.signal.aborted) {
|
||||
sendMessage(res, {
|
||||
final: true,
|
||||
conversation,
|
||||
title: conversation.title,
|
||||
requestMessage: userMessage,
|
||||
responseMessage: response,
|
||||
});
|
||||
res.end();
|
||||
|
||||
await saveMessage(
|
||||
req,
|
||||
{ ...response, user },
|
||||
{ context: 'api/server/controllers/agents/request.js - response end' },
|
||||
);
|
||||
}
|
||||
|
||||
if (!client.skipSaveUserMessage) {
|
||||
await saveMessage(req, userMessage, {
|
||||
context: 'api/server/controllers/agents/request.js - don\'t skip saving user message',
|
||||
});
|
||||
}
|
||||
|
||||
if (addTitle && parentMessageId === Constants.NO_PARENT && newConvo) {
|
||||
addTitle(req, {
|
||||
text,
|
||||
response,
|
||||
client,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
handleAbortError(res, req, error, {
|
||||
conversationId,
|
||||
sender,
|
||||
messageId: responseMessageId,
|
||||
parentMessageId: userMessageId ?? parentMessageId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AgentController;
|
||||
67
api/server/controllers/agents/run.js
Normal file
67
api/server/controllers/agents/run.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const { Run, Providers } = require('@librechat/agents');
|
||||
const { providerEndpointMap } = require('librechat-data-provider');
|
||||
|
||||
/**
|
||||
* @typedef {import('@librechat/agents').t} t
|
||||
* @typedef {import('@librechat/agents').StreamEventData} StreamEventData
|
||||
* @typedef {import('@librechat/agents').ClientOptions} ClientOptions
|
||||
* @typedef {import('@librechat/agents').EventHandler} EventHandler
|
||||
* @typedef {import('@librechat/agents').GraphEvents} GraphEvents
|
||||
* @typedef {import('@librechat/agents').IState} IState
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new Run instance with custom handlers and configuration.
|
||||
*
|
||||
* @param {Object} options - The options for creating the Run instance.
|
||||
* @param {ServerRequest} [options.req] - The server request.
|
||||
* @param {string | undefined} [options.runId] - Optional run ID; otherwise, a new run ID will be generated.
|
||||
* @param {Agent} options.agent - The agent for this run.
|
||||
* @param {StructuredTool[] | undefined} [options.tools] - The tools to use in the run.
|
||||
* @param {Record<string, StructuredTool[]> | undefined} [options.toolMap] - The tool map for the run.
|
||||
* @param {Record<GraphEvents, EventHandler> | undefined} [options.customHandlers] - Custom event handlers.
|
||||
* @param {ClientOptions} [options.modelOptions] - Optional model to use; if not provided, it will use the default from modelMap.
|
||||
* @param {boolean} [options.streaming=true] - Whether to use streaming.
|
||||
* @param {boolean} [options.streamUsage=true] - Whether to stream usage information.
|
||||
* @returns {Promise<Run<IState>>} A promise that resolves to a new Run instance.
|
||||
*/
|
||||
async function createRun({
|
||||
runId,
|
||||
tools,
|
||||
agent,
|
||||
toolMap,
|
||||
modelOptions,
|
||||
customHandlers,
|
||||
streaming = true,
|
||||
streamUsage = true,
|
||||
}) {
|
||||
const llmConfig = Object.assign(
|
||||
{
|
||||
provider: providerEndpointMap[agent.provider],
|
||||
streaming,
|
||||
streamUsage,
|
||||
},
|
||||
modelOptions,
|
||||
);
|
||||
|
||||
const graphConfig = {
|
||||
runId,
|
||||
llmConfig,
|
||||
tools,
|
||||
toolMap,
|
||||
instructions: agent.instructions,
|
||||
additional_instructions: agent.additional_instructions,
|
||||
};
|
||||
|
||||
// TEMPORARY FOR TESTING
|
||||
if (agent.provider === Providers.ANTHROPIC) {
|
||||
graphConfig.streamBuffer = 2000;
|
||||
}
|
||||
|
||||
return Run.create({
|
||||
graphConfig,
|
||||
customHandlers,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { createRun };
|
||||
239
api/server/controllers/agents/v1.js
Normal file
239
api/server/controllers/agents/v1.js
Normal file
@@ -0,0 +1,239 @@
|
||||
const { nanoid } = require('nanoid');
|
||||
const { FileContext, Constants } = require('librechat-data-provider');
|
||||
const {
|
||||
getAgent,
|
||||
createAgent,
|
||||
updateAgent,
|
||||
deleteAgent,
|
||||
getListAgents,
|
||||
} = require('~/models/Agent');
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
const { uploadImageBuffer } = require('~/server/services/Files/process');
|
||||
const { getProjectByName } = require('~/models/Project');
|
||||
const { updateAgentProjects } = require('~/models/Agent');
|
||||
const { deleteFileByFilter } = require('~/models/File');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Creates an Agent.
|
||||
* @route POST /Agents
|
||||
* @param {ServerRequest} req - The request object.
|
||||
* @param {AgentCreateParams} req.body - The request body.
|
||||
* @param {ServerResponse} res - The response object.
|
||||
* @returns {Agent} 201 - success response - application/json
|
||||
*/
|
||||
const createAgentHandler = async (req, res) => {
|
||||
try {
|
||||
const { tools = [], provider, name, description, instructions, model, ...agentData } = req.body;
|
||||
const { id: userId } = req.user;
|
||||
|
||||
agentData.tools = tools
|
||||
.map((tool) => (typeof tool === 'string' ? req.app.locals.availableTools[tool] : tool))
|
||||
.filter(Boolean);
|
||||
|
||||
Object.assign(agentData, {
|
||||
author: userId,
|
||||
name,
|
||||
description,
|
||||
instructions,
|
||||
provider,
|
||||
model,
|
||||
});
|
||||
|
||||
agentData.id = `agent_${nanoid()}`;
|
||||
const agent = await createAgent(agentData);
|
||||
res.status(201).json(agent);
|
||||
} catch (error) {
|
||||
logger.error('[/Agents] Error creating agent', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves an Agent by ID.
|
||||
* @route GET /Agents/:id
|
||||
* @param {object} req - Express Request
|
||||
* @param {object} req.params - Request params
|
||||
* @param {string} req.params.id - Agent identifier.
|
||||
* @param {object} req.user - Authenticated user information
|
||||
* @param {string} req.user.id - User ID
|
||||
* @returns {Promise<Agent>} 200 - success response - application/json
|
||||
* @returns {Error} 404 - Agent not found
|
||||
*/
|
||||
const getAgentHandler = async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const author = req.user.id;
|
||||
|
||||
let query = { id, author };
|
||||
|
||||
const globalProject = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, ['agentIds']);
|
||||
if (globalProject && (globalProject.agentIds?.length ?? 0) > 0) {
|
||||
query = {
|
||||
$or: [{ id, $in: globalProject.agentIds }, query],
|
||||
};
|
||||
}
|
||||
|
||||
const agent = await getAgent(query);
|
||||
|
||||
if (!agent) {
|
||||
return res.status(404).json({ error: 'Agent not found' });
|
||||
}
|
||||
|
||||
if (agent.author !== author) {
|
||||
delete agent.author;
|
||||
}
|
||||
|
||||
return res.status(200).json(agent);
|
||||
} catch (error) {
|
||||
logger.error('[/Agents/:id] Error retrieving agent', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates an Agent.
|
||||
* @route PATCH /Agents/:id
|
||||
* @param {object} req - Express Request
|
||||
* @param {object} req.params - Request params
|
||||
* @param {string} req.params.id - Agent identifier.
|
||||
* @param {AgentUpdateParams} req.body - The Agent update parameters.
|
||||
* @returns {Agent} 200 - success response - application/json
|
||||
*/
|
||||
const updateAgentHandler = async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const { projectIds, removeProjectIds, ...updateData } = req.body;
|
||||
|
||||
let updatedAgent;
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
updatedAgent = await updateAgent({ id, author: req.user.id }, updateData);
|
||||
}
|
||||
|
||||
if (projectIds || removeProjectIds) {
|
||||
updatedAgent = await updateAgentProjects(id, projectIds, removeProjectIds);
|
||||
}
|
||||
|
||||
return res.json(updatedAgent);
|
||||
} catch (error) {
|
||||
logger.error('[/Agents/:id] Error updating Agent', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an Agent based on the provided ID.
|
||||
* @route DELETE /Agents/:id
|
||||
* @param {object} req - Express Request
|
||||
* @param {object} req.params - Request params
|
||||
* @param {string} req.params.id - Agent identifier.
|
||||
* @returns {Agent} 200 - success response - application/json
|
||||
*/
|
||||
const deleteAgentHandler = async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const agent = await getAgent({ id });
|
||||
if (!agent) {
|
||||
return res.status(404).json({ error: 'Agent not found' });
|
||||
}
|
||||
await deleteAgent({ id, author: req.user.id });
|
||||
return res.json({ message: 'Agent deleted' });
|
||||
} catch (error) {
|
||||
logger.error('[/Agents/:id] Error deleting Agent', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @route GET /Agents
|
||||
* @param {object} req - Express Request
|
||||
* @param {object} req.query - Request query
|
||||
* @param {string} [req.query.user] - The user ID of the agent's author.
|
||||
* @returns {Promise<AgentListResponse>} 200 - success response - application/json
|
||||
*/
|
||||
const getListAgentsHandler = async (req, res) => {
|
||||
try {
|
||||
const data = await getListAgents({
|
||||
author: req.user.id,
|
||||
});
|
||||
return res.json(data);
|
||||
} catch (error) {
|
||||
logger.error('[/Agents] Error listing Agents', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads and updates an avatar for a specific agent.
|
||||
* @route POST /avatar/:agent_id
|
||||
* @param {object} req - Express Request
|
||||
* @param {object} req.params - Request params
|
||||
* @param {string} req.params.agent_id - The ID of the agent.
|
||||
* @param {Express.Multer.File} req.file - The avatar image file.
|
||||
* @param {object} req.body - Request body
|
||||
* @param {string} [req.body.avatar] - Optional avatar for the agent's avatar.
|
||||
* @returns {Object} 200 - success response - application/json
|
||||
*/
|
||||
const uploadAgentAvatarHandler = async (req, res) => {
|
||||
try {
|
||||
const { agent_id } = req.params;
|
||||
if (!agent_id) {
|
||||
return res.status(400).json({ message: 'Agent ID is required' });
|
||||
}
|
||||
|
||||
let { avatar: _avatar = '{}' } = req.body;
|
||||
|
||||
const image = await uploadImageBuffer({
|
||||
req,
|
||||
context: FileContext.avatar,
|
||||
metadata: {
|
||||
buffer: req.file.buffer,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
_avatar = JSON.parse(_avatar);
|
||||
} catch (error) {
|
||||
logger.error('[/avatar/:agent_id] Error parsing avatar', error);
|
||||
_avatar = {};
|
||||
}
|
||||
|
||||
if (_avatar && _avatar.source) {
|
||||
const { deleteFile } = getStrategyFunctions(_avatar.source);
|
||||
try {
|
||||
await deleteFile(req, { filepath: _avatar.filepath });
|
||||
await deleteFileByFilter({ filepath: _avatar.filepath });
|
||||
} catch (error) {
|
||||
logger.error('[/avatar/:agent_id] Error deleting old avatar', error);
|
||||
}
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
|
||||
const data = {
|
||||
avatar: {
|
||||
filepath: image.filepath,
|
||||
source: req.app.locals.fileStrategy,
|
||||
},
|
||||
};
|
||||
|
||||
promises.push(await updateAgent({ id: agent_id, author: req.user.id }, data));
|
||||
|
||||
const resolved = await Promise.all(promises);
|
||||
res.status(201).json(resolved[0]);
|
||||
} catch (error) {
|
||||
const message = 'An error occurred while updating the Agent Avatar';
|
||||
logger.error(message, error);
|
||||
res.status(500).json({ message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createAgent: createAgentHandler,
|
||||
getAgent: getAgentHandler,
|
||||
updateAgent: updateAgentHandler,
|
||||
deleteAgent: deleteAgentHandler,
|
||||
getListAgents: getListAgentsHandler,
|
||||
uploadAgentAvatar: uploadAgentAvatarHandler,
|
||||
};
|
||||
@@ -64,7 +64,7 @@ const _listAssistants = async ({ req, res, version, query }) => {
|
||||
* @param {object} params.res - The response object, used for initializing the client.
|
||||
* @param {string} params.version - The API version to use.
|
||||
* @param {Omit<AssistantListParams, 'endpoint'>} params.query - The query parameters to list assistants (e.g., limit, order).
|
||||
* @returns {Promise<object>} A promise that resolves to the response from the `openai.beta.assistants.list` method call.
|
||||
* @returns {Promise<Array<Assistant>>} A promise that resolves to the response from the `openai.beta.assistants.list` method call.
|
||||
*/
|
||||
const listAllAssistants = async ({ req, res, version, query }) => {
|
||||
/** @type {{ openai: OpenAIClient }} */
|
||||
|
||||
@@ -18,7 +18,9 @@ const createAssistant = async (req, res) => {
|
||||
try {
|
||||
const { openai } = await getOpenAIClient({ req, res });
|
||||
|
||||
const { tools = [], endpoint, ...assistantData } = req.body;
|
||||
const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body;
|
||||
delete assistantData.conversation_starters;
|
||||
|
||||
assistantData.tools = tools
|
||||
.map((tool) => {
|
||||
if (typeof tool !== 'string') {
|
||||
@@ -41,11 +43,22 @@ const createAssistant = async (req, res) => {
|
||||
};
|
||||
|
||||
const assistant = await openai.beta.assistants.create(assistantData);
|
||||
const promise = updateAssistantDoc({ assistant_id: assistant.id }, { user: req.user.id });
|
||||
|
||||
const createData = { user: req.user.id };
|
||||
if (conversation_starters) {
|
||||
createData.conversation_starters = conversation_starters;
|
||||
}
|
||||
|
||||
const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData);
|
||||
|
||||
if (azureModelIdentifier) {
|
||||
assistant.model = azureModelIdentifier;
|
||||
}
|
||||
await promise;
|
||||
|
||||
if (document.conversation_starters) {
|
||||
assistant.conversation_starters = document.conversation_starters;
|
||||
}
|
||||
|
||||
logger.debug('/assistants/', assistant);
|
||||
res.status(201).json(assistant);
|
||||
} catch (error) {
|
||||
@@ -88,7 +101,7 @@ const patchAssistant = async (req, res) => {
|
||||
await validateAuthor({ req, openai });
|
||||
|
||||
const assistant_id = req.params.id;
|
||||
const { endpoint: _e, ...updateData } = req.body;
|
||||
const { endpoint: _e, conversation_starters, ...updateData } = req.body;
|
||||
updateData.tools = (updateData.tools ?? [])
|
||||
.map((tool) => {
|
||||
if (typeof tool !== 'string') {
|
||||
@@ -104,6 +117,15 @@ const patchAssistant = async (req, res) => {
|
||||
}
|
||||
|
||||
const updatedAssistant = await openai.beta.assistants.update(assistant_id, updateData);
|
||||
|
||||
if (conversation_starters !== undefined) {
|
||||
const conversationStartersUpdate = await updateAssistantDoc(
|
||||
{ assistant_id },
|
||||
{ conversation_starters },
|
||||
);
|
||||
updatedAssistant.conversation_starters = conversationStartersUpdate.conversation_starters;
|
||||
}
|
||||
|
||||
res.json(updatedAssistant);
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/:id] Error updating assistant', error);
|
||||
@@ -153,6 +175,32 @@ const listAssistants = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter assistants based on configuration.
|
||||
*
|
||||
* @param {object} params - The parameters object.
|
||||
* @param {string} params.userId - The user ID to filter private assistants.
|
||||
* @param {AssistantDocument[]} params.assistants - The list of assistants to filter.
|
||||
* @param {Partial<TAssistantEndpoint>} [params.assistantsConfig] - The assistant configuration.
|
||||
* @returns {AssistantDocument[]} - The filtered list of assistants.
|
||||
*/
|
||||
function filterAssistantDocs({ documents, userId, assistantsConfig = {} }) {
|
||||
const { supportedIds, excludedIds, privateAssistants } = assistantsConfig;
|
||||
const removeUserId = (doc) => {
|
||||
const { user: _u, ...document } = doc;
|
||||
return document;
|
||||
};
|
||||
|
||||
if (privateAssistants) {
|
||||
return documents.filter((doc) => userId === doc.user.toString()).map(removeUserId);
|
||||
} else if (supportedIds?.length) {
|
||||
return documents.filter((doc) => supportedIds.includes(doc.assistant_id)).map(removeUserId);
|
||||
} else if (excludedIds?.length) {
|
||||
return documents.filter((doc) => !excludedIds.includes(doc.assistant_id)).map(removeUserId);
|
||||
}
|
||||
return documents.map(removeUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of the user's assistant documents (metadata saved to database).
|
||||
* @route GET /assistants/documents
|
||||
@@ -160,7 +208,25 @@ const listAssistants = async (req, res) => {
|
||||
*/
|
||||
const getAssistantDocuments = async (req, res) => {
|
||||
try {
|
||||
res.json(await getAssistants({ user: req.user.id }));
|
||||
const endpoint = req.query;
|
||||
const assistantsConfig = req.app.locals[endpoint];
|
||||
const documents = await getAssistants(
|
||||
{},
|
||||
{
|
||||
user: 1,
|
||||
assistant_id: 1,
|
||||
conversation_starters: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
},
|
||||
);
|
||||
|
||||
const docs = filterAssistantDocs({
|
||||
documents,
|
||||
userId: req.user.id,
|
||||
assistantsConfig,
|
||||
});
|
||||
res.json(docs);
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/documents] Error listing assistant documents', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
|
||||
@@ -16,7 +16,9 @@ const createAssistant = async (req, res) => {
|
||||
/** @type {{ openai: OpenAIClient }} */
|
||||
const { openai } = await getOpenAIClient({ req, res });
|
||||
|
||||
const { tools = [], endpoint, ...assistantData } = req.body;
|
||||
const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body;
|
||||
delete assistantData.conversation_starters;
|
||||
|
||||
assistantData.tools = tools
|
||||
.map((tool) => {
|
||||
if (typeof tool !== 'string') {
|
||||
@@ -39,11 +41,22 @@ const createAssistant = async (req, res) => {
|
||||
};
|
||||
|
||||
const assistant = await openai.beta.assistants.create(assistantData);
|
||||
const promise = updateAssistantDoc({ assistant_id: assistant.id }, { user: req.user.id });
|
||||
|
||||
const createData = { user: req.user.id };
|
||||
if (conversation_starters) {
|
||||
createData.conversation_starters = conversation_starters;
|
||||
}
|
||||
|
||||
const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData);
|
||||
|
||||
if (azureModelIdentifier) {
|
||||
assistant.model = azureModelIdentifier;
|
||||
}
|
||||
await promise;
|
||||
|
||||
if (document.conversation_starters) {
|
||||
assistant.conversation_starters = document.conversation_starters;
|
||||
}
|
||||
|
||||
logger.debug('/assistants/', assistant);
|
||||
res.status(201).json(assistant);
|
||||
} catch (error) {
|
||||
@@ -64,6 +77,17 @@ const createAssistant = async (req, res) => {
|
||||
const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
|
||||
await validateAuthor({ req, openai });
|
||||
const tools = [];
|
||||
let conversation_starters = null;
|
||||
|
||||
if (updateData?.conversation_starters) {
|
||||
const conversationStartersUpdate = await updateAssistantDoc(
|
||||
{ assistant_id: assistant_id },
|
||||
{ conversation_starters: updateData.conversation_starters },
|
||||
);
|
||||
conversation_starters = conversationStartersUpdate.conversation_starters;
|
||||
|
||||
delete updateData.conversation_starters;
|
||||
}
|
||||
|
||||
let hasFileSearch = false;
|
||||
for (const tool of updateData.tools ?? []) {
|
||||
@@ -108,7 +132,13 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
|
||||
updateData.model = openai.locals.azureOptions.azureOpenAIApiDeploymentName;
|
||||
}
|
||||
|
||||
return await openai.beta.assistants.update(assistant_id, updateData);
|
||||
const assistant = await openai.beta.assistants.update(assistant_id, updateData);
|
||||
|
||||
if (conversation_starters) {
|
||||
assistant.conversation_starters = conversation_starters;
|
||||
}
|
||||
|
||||
return assistant;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,8 @@ const express = require('express');
|
||||
const compression = require('compression');
|
||||
const passport = require('passport');
|
||||
const mongoSanitize = require('express-mongo-sanitize');
|
||||
const fs = require('fs');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const { jwtLogin, passportLogin } = require('~/strategies');
|
||||
const { connectDb, indexSync } = require('~/lib/db');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
@@ -37,6 +39,9 @@ const startServer = async () => {
|
||||
app.disable('x-powered-by');
|
||||
await AppService(app);
|
||||
|
||||
const indexPath = path.join(app.locals.paths.dist, 'index.html');
|
||||
const indexHTML = fs.readFileSync(indexPath, 'utf8');
|
||||
|
||||
app.get('/health', (_req, res) => res.status(200).send('OK'));
|
||||
|
||||
/* Middleware */
|
||||
@@ -50,6 +55,7 @@ const startServer = async () => {
|
||||
app.use(staticCache(app.locals.paths.assets));
|
||||
app.set('trust proxy', 1); /* trust first proxy */
|
||||
app.use(cors());
|
||||
app.use(cookieParser());
|
||||
|
||||
if (!isEnabled(DISABLE_COMPRESSION)) {
|
||||
app.use(compression());
|
||||
@@ -99,10 +105,16 @@ const startServer = async () => {
|
||||
app.use('/images/', validateImageRequest, routes.staticRoute);
|
||||
app.use('/api/share', routes.share);
|
||||
app.use('/api/roles', routes.roles);
|
||||
app.use('/api/agents', routes.agents);
|
||||
app.use('/api/bedrock', routes.bedrock);
|
||||
|
||||
app.use('/api/tags', routes.tags);
|
||||
|
||||
app.use((req, res) => {
|
||||
res.sendFile(path.join(app.locals.paths.dist, 'index.html'));
|
||||
// Replace lang attribute in index.html with lang from cookies or accept-language header
|
||||
const lang = req.cookies.lang || req.headers['accept-language']?.split(',')[0] || 'en-US';
|
||||
const updatedIndexHtml = indexHTML.replace(/lang="en-US"/g, `lang="${lang}"`);
|
||||
res.send(updatedIndexHtml);
|
||||
});
|
||||
|
||||
app.listen(port, host, () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { isAssistantsEndpoint } = require('librechat-data-provider');
|
||||
const { isAssistantsEndpoint, ErrorTypes } = require('librechat-data-provider');
|
||||
const { sendMessage, sendError, countTokens, isEnabled } = require('~/server/utils');
|
||||
const { truncateText, smartTruncateText } = require('~/app/clients/prompts');
|
||||
const clearPendingReq = require('~/cache/clearPendingReq');
|
||||
@@ -107,7 +107,7 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
|
||||
finish_reason: 'incomplete',
|
||||
endpoint: endpointOption.endpoint,
|
||||
iconURL: endpointOption.iconURL,
|
||||
model: endpointOption.modelOptions.model,
|
||||
model: endpointOption.modelOptions?.model ?? endpointOption.model_parameters?.model,
|
||||
unfinished: false,
|
||||
error: false,
|
||||
isCreatedByUser: false,
|
||||
@@ -165,10 +165,14 @@ const handleAbortError = async (res, req, error, data) => {
|
||||
);
|
||||
}
|
||||
|
||||
const errorText = error?.message?.includes('"type"')
|
||||
let errorText = error?.message?.includes('"type"')
|
||||
? error.message
|
||||
: 'An error occurred while processing your request. Please contact the Admin.';
|
||||
|
||||
if (error?.type === ErrorTypes.INVALID_REQUEST) {
|
||||
errorText = `{"type":"${ErrorTypes.INVALID_REQUEST}"}`;
|
||||
}
|
||||
|
||||
const respondWithError = async (partialText) => {
|
||||
let options = {
|
||||
sender,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const { parseCompactConvo, EModelEndpoint } = require('librechat-data-provider');
|
||||
const { parseCompactConvo, EModelEndpoint, isAgentsEndpoint } = require('librechat-data-provider');
|
||||
const { getModelsConfig } = require('~/server/controllers/ModelController');
|
||||
const azureAssistants = require('~/server/services/Endpoints/azureAssistants');
|
||||
const assistants = require('~/server/services/Endpoints/assistants');
|
||||
const gptPlugins = require('~/server/services/Endpoints/gptPlugins');
|
||||
const { processFiles } = require('~/server/services/Files/process');
|
||||
const anthropic = require('~/server/services/Endpoints/anthropic');
|
||||
const bedrock = require('~/server/services/Endpoints/bedrock');
|
||||
const openAI = require('~/server/services/Endpoints/openAI');
|
||||
const agents = require('~/server/services/Endpoints/agents');
|
||||
const custom = require('~/server/services/Endpoints/custom');
|
||||
const google = require('~/server/services/Endpoints/google');
|
||||
const enforceModelSpec = require('./enforceModelSpec');
|
||||
@@ -15,6 +17,8 @@ const buildFunction = {
|
||||
[EModelEndpoint.openAI]: openAI.buildOptions,
|
||||
[EModelEndpoint.google]: google.buildOptions,
|
||||
[EModelEndpoint.custom]: custom.buildOptions,
|
||||
[EModelEndpoint.agents]: agents.buildOptions,
|
||||
[EModelEndpoint.bedrock]: bedrock.buildOptions,
|
||||
[EModelEndpoint.azureOpenAI]: openAI.buildOptions,
|
||||
[EModelEndpoint.anthropic]: anthropic.buildOptions,
|
||||
[EModelEndpoint.gptPlugins]: gptPlugins.buildOptions,
|
||||
@@ -59,12 +63,13 @@ async function buildEndpointOption(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
req.body.endpointOption = buildFunction[endpointType ?? endpoint](
|
||||
endpoint,
|
||||
parsedBody,
|
||||
endpointType,
|
||||
);
|
||||
const endpointFn = buildFunction[endpointType ?? endpoint];
|
||||
const builder = isAgentsEndpoint(endpoint) ? (...args) => endpointFn(req, ...args) : endpointFn;
|
||||
|
||||
// TODO: use object params
|
||||
req.body.endpointOption = builder(endpoint, parsedBody, endpointType);
|
||||
|
||||
// TODO: use `getModelsConfig` only when necessary
|
||||
const modelsConfig = await getModelsConfig(req);
|
||||
req.body.endpointOption.modelsConfig = modelsConfig;
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const { SystemRoles } = require('librechat-data-provider');
|
||||
const { getRoleByName } = require('~/models/Role');
|
||||
|
||||
/**
|
||||
@@ -17,10 +16,6 @@ const generateCheckAccess = (permissionType, permissions, bodyProps = {}) => {
|
||||
return res.status(401).json({ message: 'Authorization required' });
|
||||
}
|
||||
|
||||
if (user.role === SystemRoles.ADMIN) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const role = await getRoleByName(user.role);
|
||||
if (role && role[permissionType]) {
|
||||
const hasAnyPermission = permissions.some((permission) => {
|
||||
|
||||
166
api/server/routes/agents/actions.js
Normal file
166
api/server/routes/agents/actions.js
Normal file
@@ -0,0 +1,166 @@
|
||||
const express = require('express');
|
||||
const { nanoid } = require('nanoid');
|
||||
const { actionDelimiter } = require('librechat-data-provider');
|
||||
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
||||
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
||||
const { getAgent, updateAgent } = require('~/models/Agent');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Retrieves all user's actions
|
||||
* @route GET /actions/
|
||||
* @param {string} req.params.id - Assistant identifier.
|
||||
* @returns {Action[]} 200 - success response - application/json
|
||||
*/
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
res.json(await getActions({ user: req.user.id }));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Adds or updates actions for a specific agent.
|
||||
* @route POST /actions/:agent_id
|
||||
* @param {string} req.params.agent_id - The ID of the agent.
|
||||
* @param {FunctionTool[]} req.body.functions - The functions to be added or updated.
|
||||
* @param {string} [req.body.action_id] - Optional ID for the action.
|
||||
* @param {ActionMetadata} req.body.metadata - Metadata for the action.
|
||||
* @returns {Object} 200 - success response - application/json
|
||||
*/
|
||||
router.post('/:agent_id', async (req, res) => {
|
||||
try {
|
||||
const { agent_id } = req.params;
|
||||
|
||||
/** @type {{ functions: FunctionTool[], action_id: string, metadata: ActionMetadata }} */
|
||||
const { functions, action_id: _action_id, metadata: _metadata } = req.body;
|
||||
if (!functions.length) {
|
||||
return res.status(400).json({ message: 'No functions provided' });
|
||||
}
|
||||
|
||||
let metadata = await encryptMetadata(_metadata);
|
||||
|
||||
let { domain } = metadata;
|
||||
domain = await domainParser(req, domain, true);
|
||||
|
||||
if (!domain) {
|
||||
return res.status(400).json({ message: 'No domain provided' });
|
||||
}
|
||||
|
||||
const action_id = _action_id ?? nanoid();
|
||||
const initialPromises = [];
|
||||
|
||||
// TODO: share agents
|
||||
initialPromises.push(getAgent({ id: agent_id, author: req.user.id }));
|
||||
if (_action_id) {
|
||||
initialPromises.push(getActions({ action_id }, true));
|
||||
}
|
||||
|
||||
/** @type {[Agent, [Action|undefined]]} */
|
||||
const [agent, actions_result] = await Promise.all(initialPromises);
|
||||
if (!agent) {
|
||||
return res.status(404).json({ message: 'Agent not found for adding action' });
|
||||
}
|
||||
|
||||
if (actions_result && actions_result.length) {
|
||||
const action = actions_result[0];
|
||||
metadata = { ...action.metadata, ...metadata };
|
||||
}
|
||||
|
||||
const { actions: _actions = [] } = agent ?? {};
|
||||
const actions = [];
|
||||
for (const action of _actions) {
|
||||
const [_action_domain, current_action_id] = action.split(actionDelimiter);
|
||||
if (current_action_id === action_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
actions.push(action);
|
||||
}
|
||||
|
||||
actions.push(`${domain}${actionDelimiter}${action_id}`);
|
||||
|
||||
/** @type {string[]}} */
|
||||
const { tools: _tools = [] } = agent;
|
||||
|
||||
const tools = _tools
|
||||
.filter((tool) => !(tool && (tool.includes(domain) || tool.includes(action_id))))
|
||||
.concat(functions.map((tool) => `${tool.function.name}${actionDelimiter}${domain}`));
|
||||
|
||||
const updatedAgent = await updateAgent(
|
||||
{ id: agent_id, author: req.user.id },
|
||||
{ tools, actions },
|
||||
);
|
||||
/** @type {[Action]} */
|
||||
const updatedAction = await updateAction(
|
||||
{ action_id },
|
||||
{ metadata, agent_id, user: req.user.id },
|
||||
);
|
||||
|
||||
const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'];
|
||||
for (let field of sensitiveFields) {
|
||||
if (updatedAction.metadata[field]) {
|
||||
delete updatedAction.metadata[field];
|
||||
}
|
||||
}
|
||||
|
||||
res.json([updatedAgent, updatedAction]);
|
||||
} catch (error) {
|
||||
const message = 'Trouble updating the Agent Action';
|
||||
logger.error(message, error);
|
||||
res.status(500).json({ message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes an action for a specific agent.
|
||||
* @route DELETE /actions/:agent_id/:action_id
|
||||
* @param {string} req.params.agent_id - The ID of the agent.
|
||||
* @param {string} req.params.action_id - The ID of the action to delete.
|
||||
* @returns {Object} 200 - success response - application/json
|
||||
*/
|
||||
router.delete('/:agent_id/:action_id', async (req, res) => {
|
||||
try {
|
||||
const { agent_id, action_id } = req.params;
|
||||
|
||||
const agent = await getAgent({ id: agent_id, author: req.user.id });
|
||||
if (!agent) {
|
||||
return res.status(404).json({ message: 'Agent not found for deleting action' });
|
||||
}
|
||||
|
||||
const { tools = [], actions = [] } = agent;
|
||||
|
||||
let domain = '';
|
||||
const updatedActions = actions.filter((action) => {
|
||||
if (action.includes(action_id)) {
|
||||
[domain] = action.split(actionDelimiter);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
domain = await domainParser(req, domain, true);
|
||||
|
||||
if (!domain) {
|
||||
return res.status(400).json({ message: 'No domain provided' });
|
||||
}
|
||||
|
||||
const updatedTools = tools.filter((tool) => !(tool && tool.includes(domain)));
|
||||
|
||||
await updateAgent(
|
||||
{ id: agent_id, author: req.user.id },
|
||||
{ tools: updatedTools, actions: updatedActions },
|
||||
);
|
||||
await deleteAction({ action_id });
|
||||
res.status(200).json({ message: 'Action deleted successfully' });
|
||||
} catch (error) {
|
||||
const message = 'Trouble deleting the Agent Action';
|
||||
logger.error(message, error);
|
||||
res.status(500).json({ message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
35
api/server/routes/agents/chat.js
Normal file
35
api/server/routes/agents/chat.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const express = require('express');
|
||||
|
||||
const router = express.Router();
|
||||
const {
|
||||
setHeaders,
|
||||
handleAbort,
|
||||
// validateModel,
|
||||
// validateEndpoint,
|
||||
buildEndpointOption,
|
||||
} = require('~/server/middleware');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/agents');
|
||||
const AgentController = require('~/server/controllers/agents/request');
|
||||
|
||||
router.post('/abort', handleAbort());
|
||||
|
||||
/**
|
||||
* @route POST /
|
||||
* @desc Chat with an assistant
|
||||
* @access Public
|
||||
* @param {express.Request} req - The request object, containing the request data.
|
||||
* @param {express.Response} res - The response object, used to send back a response.
|
||||
* @returns {void}
|
||||
*/
|
||||
router.post(
|
||||
'/',
|
||||
// validateModel,
|
||||
// validateEndpoint,
|
||||
buildEndpointOption,
|
||||
setHeaders,
|
||||
async (req, res, next) => {
|
||||
await AgentController(req, res, next, initializeClient);
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
21
api/server/routes/agents/index.js
Normal file
21
api/server/routes/agents/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
uaParser,
|
||||
checkBan,
|
||||
requireJwtAuth,
|
||||
// concurrentLimiter,
|
||||
// messageIpLimiter,
|
||||
// messageUserLimiter,
|
||||
} = require('~/server/middleware');
|
||||
|
||||
const v1 = require('./v1');
|
||||
const chat = require('./chat');
|
||||
|
||||
router.use(requireJwtAuth);
|
||||
router.use(checkBan);
|
||||
router.use(uaParser);
|
||||
router.use('/', v1);
|
||||
router.use('/chat', chat);
|
||||
|
||||
module.exports = router;
|
||||
94
api/server/routes/agents/v1.js
Normal file
94
api/server/routes/agents/v1.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const multer = require('multer');
|
||||
const express = require('express');
|
||||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
||||
const v1 = require('~/server/controllers/agents/v1');
|
||||
const actions = require('./actions');
|
||||
|
||||
const upload = multer();
|
||||
const router = express.Router();
|
||||
|
||||
const checkAgentAccess = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]);
|
||||
const checkAgentCreate = generateCheckAccess(PermissionTypes.AGENTS, [
|
||||
Permissions.USE,
|
||||
Permissions.CREATE,
|
||||
]);
|
||||
|
||||
const checkGlobalAgentShare = generateCheckAccess(
|
||||
PermissionTypes.AGENTS,
|
||||
[Permissions.USE, Permissions.CREATE],
|
||||
{
|
||||
[Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'],
|
||||
},
|
||||
);
|
||||
|
||||
router.use(requireJwtAuth);
|
||||
router.use(checkAgentAccess);
|
||||
|
||||
/**
|
||||
* Agent actions route.
|
||||
* @route GET|POST /agents/actions
|
||||
*/
|
||||
router.use('/actions', actions);
|
||||
|
||||
/**
|
||||
* Get a list of available tools for agents.
|
||||
* @route GET /agents/tools
|
||||
* @returns {TPlugin[]} 200 - application/json
|
||||
*/
|
||||
router.use('/tools', (req, res) => {
|
||||
res.json([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates an agent.
|
||||
* @route POST /agents
|
||||
* @param {AgentCreateParams} req.body - The agent creation parameters.
|
||||
* @returns {Agent} 201 - Success response - application/json
|
||||
*/
|
||||
router.post('/', checkAgentCreate, v1.createAgent);
|
||||
|
||||
/**
|
||||
* Retrieves an agent.
|
||||
* @route GET /agents/:id
|
||||
* @param {string} req.params.id - Agent identifier.
|
||||
* @returns {Agent} 200 - Success response - application/json
|
||||
*/
|
||||
router.get('/:id', checkAgentAccess, v1.getAgent);
|
||||
|
||||
/**
|
||||
* Updates an agent.
|
||||
* @route PATCH /agents/:id
|
||||
* @param {string} req.params.id - Agent identifier.
|
||||
* @param {AgentUpdateParams} req.body - The agent update parameters.
|
||||
* @returns {Agent} 200 - Success response - application/json
|
||||
*/
|
||||
router.patch('/:id', checkGlobalAgentShare, v1.updateAgent);
|
||||
|
||||
/**
|
||||
* Deletes an agent.
|
||||
* @route DELETE /agents/:id
|
||||
* @param {string} req.params.id - Agent identifier.
|
||||
* @returns {Agent} 200 - success response - application/json
|
||||
*/
|
||||
router.delete('/:id', checkAgentCreate, v1.deleteAgent);
|
||||
|
||||
/**
|
||||
* Returns a list of agents.
|
||||
* @route GET /agents
|
||||
* @param {AgentListParams} req.query - The agent list parameters for pagination and sorting.
|
||||
* @returns {AgentListResponse} 200 - success response - application/json
|
||||
*/
|
||||
router.get('/', checkAgentAccess, v1.getListAgents);
|
||||
|
||||
/**
|
||||
* Uploads and updates an avatar for a specific agent.
|
||||
* @route POST /avatar/:agent_id
|
||||
* @param {string} req.params.agent_id - The ID of the agent.
|
||||
* @param {Express.Multer.File} req.file - The avatar image file.
|
||||
* @param {string} [req.body.metadata] - Optional metadata for the agent's avatar.
|
||||
* @returns {Object} 200 - success response - application/json
|
||||
*/
|
||||
router.post('/avatar/:agent_id', checkAgentAccess, upload.single('file'), v1.uploadAgentAvatar);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,5 +1,5 @@
|
||||
const { v4 } = require('uuid');
|
||||
const express = require('express');
|
||||
const { nanoid } = require('nanoid');
|
||||
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
||||
const { actionDelimiter, EModelEndpoint } = require('librechat-data-provider');
|
||||
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
||||
@@ -9,20 +9,6 @@ const { logger } = require('~/config');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Retrieves all user's actions
|
||||
* @route GET /actions/
|
||||
* @param {string} req.params.id - Assistant identifier.
|
||||
* @returns {Action[]} 200 - success response - application/json
|
||||
*/
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
res.json(await getActions());
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Adds or updates actions for a specific assistant.
|
||||
* @route POST /actions/:assistant_id
|
||||
@@ -51,7 +37,7 @@ router.post('/:assistant_id', async (req, res) => {
|
||||
return res.status(400).json({ message: 'No domain provided' });
|
||||
}
|
||||
|
||||
const action_id = _action_id ?? v4();
|
||||
const action_id = _action_id ?? nanoid();
|
||||
const initialPromises = [];
|
||||
|
||||
const { openai } = await getOpenAIClient({ req, res });
|
||||
@@ -178,6 +164,10 @@ router.delete('/:assistant_id/:action_id/:model', async (req, res) => {
|
||||
|
||||
domain = await domainParser(req, domain, true);
|
||||
|
||||
if (!domain) {
|
||||
return res.status(400).json({ message: 'No domain provided' });
|
||||
}
|
||||
|
||||
const updatedTools = tools.filter(
|
||||
(tool) => !(tool.function && tool.function.name.includes(domain)),
|
||||
);
|
||||
|
||||
13
api/server/routes/assistants/documents.js
Normal file
13
api/server/routes/assistants/documents.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const express = require('express');
|
||||
const controllers = require('~/server/controllers/assistants/v1');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Returns a list of the user's assistant documents (metadata saved to database).
|
||||
* @route GET /assistants/documents
|
||||
* @returns {AssistantDocument[]} 200 - success response - application/json
|
||||
*/
|
||||
router.get('/', controllers.getAssistantDocuments);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,6 +1,7 @@
|
||||
const multer = require('multer');
|
||||
const express = require('express');
|
||||
const controllers = require('~/server/controllers/assistants/v1');
|
||||
const documents = require('./documents');
|
||||
const actions = require('./actions');
|
||||
const tools = require('./tools');
|
||||
|
||||
@@ -20,6 +21,13 @@ router.use('/actions', actions);
|
||||
*/
|
||||
router.use('/tools', tools);
|
||||
|
||||
/**
|
||||
* Create an assistant.
|
||||
* @route GET /assistants/documents
|
||||
* @returns {AssistantDocument[]} 200 - application/json
|
||||
*/
|
||||
router.use('/documents', documents);
|
||||
|
||||
/**
|
||||
* Create an assistant.
|
||||
* @route POST /assistants
|
||||
@@ -61,13 +69,6 @@ router.delete('/:id', controllers.deleteAssistant);
|
||||
*/
|
||||
router.get('/', controllers.listAssistants);
|
||||
|
||||
/**
|
||||
* Returns a list of the user's assistant documents (metadata saved to database).
|
||||
* @route GET /assistants/documents
|
||||
* @returns {AssistantDocument[]} 200 - success response - application/json
|
||||
*/
|
||||
router.get('/documents', controllers.getAssistantDocuments);
|
||||
|
||||
/**
|
||||
* Uploads and updates an avatar for a specific assistant.
|
||||
* @route POST /avatar/:assistant_id
|
||||
|
||||
@@ -2,6 +2,7 @@ const multer = require('multer');
|
||||
const express = require('express');
|
||||
const v1 = require('~/server/controllers/assistants/v1');
|
||||
const v2 = require('~/server/controllers/assistants/v2');
|
||||
const documents = require('./documents');
|
||||
const actions = require('./actions');
|
||||
const tools = require('./tools');
|
||||
|
||||
@@ -21,6 +22,13 @@ router.use('/actions', actions);
|
||||
*/
|
||||
router.use('/tools', tools);
|
||||
|
||||
/**
|
||||
* Create an assistant.
|
||||
* @route GET /assistants/documents
|
||||
* @returns {AssistantDocument[]} 200 - application/json
|
||||
*/
|
||||
router.use('/documents', documents);
|
||||
|
||||
/**
|
||||
* Create an assistant.
|
||||
* @route POST /assistants
|
||||
@@ -62,13 +70,6 @@ router.delete('/:id', v1.deleteAssistant);
|
||||
*/
|
||||
router.get('/', v1.listAssistants);
|
||||
|
||||
/**
|
||||
* Returns a list of the user's assistant documents (metadata saved to database).
|
||||
* @route GET /assistants/documents
|
||||
* @returns {AssistantDocument[]} 200 - success response - application/json
|
||||
*/
|
||||
router.get('/documents', v1.getAssistantDocuments);
|
||||
|
||||
/**
|
||||
* Uploads and updates an avatar for a specific assistant.
|
||||
* @route POST /avatar/:assistant_id
|
||||
|
||||
36
api/server/routes/bedrock/chat.js
Normal file
36
api/server/routes/bedrock/chat.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const express = require('express');
|
||||
|
||||
const router = express.Router();
|
||||
const {
|
||||
setHeaders,
|
||||
handleAbort,
|
||||
// validateModel,
|
||||
// validateEndpoint,
|
||||
buildEndpointOption,
|
||||
} = require('~/server/middleware');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/bedrock');
|
||||
const AgentController = require('~/server/controllers/agents/request');
|
||||
const addTitle = require('~/server/services/Endpoints/bedrock/title');
|
||||
|
||||
router.post('/abort', handleAbort());
|
||||
|
||||
/**
|
||||
* @route POST /
|
||||
* @desc Chat with an assistant
|
||||
* @access Public
|
||||
* @param {express.Request} req - The request object, containing the request data.
|
||||
* @param {express.Response} res - The response object, used to send back a response.
|
||||
* @returns {void}
|
||||
*/
|
||||
router.post(
|
||||
'/',
|
||||
// validateModel,
|
||||
// validateEndpoint,
|
||||
buildEndpointOption,
|
||||
setHeaders,
|
||||
async (req, res, next) => {
|
||||
await AgentController(req, res, next, initializeClient, addTitle);
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
19
api/server/routes/bedrock/index.js
Normal file
19
api/server/routes/bedrock/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
uaParser,
|
||||
checkBan,
|
||||
requireJwtAuth,
|
||||
// concurrentLimiter,
|
||||
// messageIpLimiter,
|
||||
// messageUserLimiter,
|
||||
} = require('~/server/middleware');
|
||||
|
||||
const chat = require('./chat');
|
||||
|
||||
router.use(requireJwtAuth);
|
||||
router.use(checkBan);
|
||||
router.use(uaParser);
|
||||
router.use('/chat', chat);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,5 +1,5 @@
|
||||
const express = require('express');
|
||||
const { CacheKeys, defaultSocialLogins } = require('librechat-data-provider');
|
||||
const { CacheKeys, defaultSocialLogins, Constants } = require('librechat-data-provider');
|
||||
const { getLdapConfig } = require('~/server/services/Config/ldap');
|
||||
const { getProjectByName } = require('~/models/Project');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
@@ -32,7 +32,7 @@ router.get('/', async function (req, res) {
|
||||
return today.getMonth() === 1 && today.getDate() === 11;
|
||||
};
|
||||
|
||||
const instanceProject = await getProjectByName('instance', '_id');
|
||||
const instanceProject = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, '_id');
|
||||
|
||||
const ldap = getLdapConfig();
|
||||
|
||||
|
||||
@@ -1,51 +1,55 @@
|
||||
const ask = require('./ask');
|
||||
const edit = require('./edit');
|
||||
const assistants = require('./assistants');
|
||||
const categories = require('./categories');
|
||||
const tokenizer = require('./tokenizer');
|
||||
const endpoints = require('./endpoints');
|
||||
const staticRoute = require('./static');
|
||||
const messages = require('./messages');
|
||||
const convos = require('./convos');
|
||||
const presets = require('./presets');
|
||||
const prompts = require('./prompts');
|
||||
const search = require('./search');
|
||||
const tokenizer = require('./tokenizer');
|
||||
const auth = require('./auth');
|
||||
const keys = require('./keys');
|
||||
const oauth = require('./oauth');
|
||||
const endpoints = require('./endpoints');
|
||||
const balance = require('./balance');
|
||||
const models = require('./models');
|
||||
const plugins = require('./plugins');
|
||||
const user = require('./user');
|
||||
const bedrock = require('./bedrock');
|
||||
const search = require('./search');
|
||||
const models = require('./models');
|
||||
const convos = require('./convos');
|
||||
const config = require('./config');
|
||||
const assistants = require('./assistants');
|
||||
const files = require('./files');
|
||||
const staticRoute = require('./static');
|
||||
const share = require('./share');
|
||||
const categories = require('./categories');
|
||||
const agents = require('./agents');
|
||||
const roles = require('./roles');
|
||||
const oauth = require('./oauth');
|
||||
const files = require('./files');
|
||||
const share = require('./share');
|
||||
const tags = require('./tags');
|
||||
const auth = require('./auth');
|
||||
const edit = require('./edit');
|
||||
const keys = require('./keys');
|
||||
const user = require('./user');
|
||||
const ask = require('./ask');
|
||||
|
||||
module.exports = {
|
||||
search,
|
||||
ask,
|
||||
edit,
|
||||
messages,
|
||||
convos,
|
||||
presets,
|
||||
prompts,
|
||||
auth,
|
||||
keys,
|
||||
oauth,
|
||||
user,
|
||||
tokenizer,
|
||||
endpoints,
|
||||
balance,
|
||||
tags,
|
||||
roles,
|
||||
oauth,
|
||||
files,
|
||||
share,
|
||||
agents,
|
||||
bedrock,
|
||||
convos,
|
||||
search,
|
||||
prompts,
|
||||
config,
|
||||
models,
|
||||
plugins,
|
||||
config,
|
||||
presets,
|
||||
balance,
|
||||
messages,
|
||||
endpoints,
|
||||
tokenizer,
|
||||
assistants,
|
||||
files,
|
||||
staticRoute,
|
||||
share,
|
||||
categories,
|
||||
roles,
|
||||
tags,
|
||||
staticRoute,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const express = require('express');
|
||||
const { ContentTypes } = require('librechat-data-provider');
|
||||
const { saveConvo, saveMessage, getMessages, updateMessage, deleteMessages } = require('~/models');
|
||||
const { requireJwtAuth, validateMessageReq } = require('~/server/middleware');
|
||||
const { countTokens } = require('~/server/utils');
|
||||
@@ -54,11 +55,50 @@ router.get('/:conversationId/:messageId', validateMessageReq, async (req, res) =
|
||||
|
||||
router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) => {
|
||||
try {
|
||||
const { messageId, model } = req.params;
|
||||
const { text } = req.body;
|
||||
const tokenCount = await countTokens(text, model);
|
||||
const result = await updateMessage(req, { messageId, text, tokenCount });
|
||||
res.status(200).json(result);
|
||||
const { conversationId, messageId } = req.params;
|
||||
const { text, index, model } = req.body;
|
||||
|
||||
if (index === undefined) {
|
||||
const tokenCount = await countTokens(text, model);
|
||||
const result = await updateMessage(req, { messageId, text, tokenCount });
|
||||
return res.status(200).json(result);
|
||||
}
|
||||
|
||||
if (typeof index !== 'number' || index < 0) {
|
||||
return res.status(400).json({ error: 'Invalid index' });
|
||||
}
|
||||
|
||||
const message = (await getMessages({ conversationId, messageId }, 'content tokenCount'))?.[0];
|
||||
if (!message) {
|
||||
return res.status(404).json({ error: 'Message not found' });
|
||||
}
|
||||
|
||||
const existingContent = message.content;
|
||||
if (!Array.isArray(existingContent) || index >= existingContent.length) {
|
||||
return res.status(400).json({ error: 'Invalid index' });
|
||||
}
|
||||
|
||||
const updatedContent = [...existingContent];
|
||||
if (!updatedContent[index]) {
|
||||
return res.status(400).json({ error: 'Content part not found' });
|
||||
}
|
||||
|
||||
if (updatedContent[index].type !== ContentTypes.TEXT) {
|
||||
return res.status(400).json({ error: 'Cannot update non-text content' });
|
||||
}
|
||||
|
||||
const oldText = updatedContent[index].text;
|
||||
updatedContent[index] = { type: ContentTypes.TEXT, text };
|
||||
|
||||
let tokenCount = message.tokenCount;
|
||||
if (tokenCount !== undefined) {
|
||||
const oldTokenCount = await countTokens(oldText, model);
|
||||
const newTokenCount = await countTokens(text, model);
|
||||
tokenCount = Math.max(0, tokenCount - oldTokenCount) + newTokenCount;
|
||||
}
|
||||
|
||||
const result = await updateMessage(req, { messageId, content: updatedContent, tokenCount });
|
||||
return res.status(200).json(result);
|
||||
} catch (error) {
|
||||
logger.error('Error updating message:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
|
||||
@@ -24,6 +24,7 @@ const checkPromptCreate = generateCheckAccess(PermissionTypes.PROMPTS, [
|
||||
Permissions.USE,
|
||||
Permissions.CREATE,
|
||||
]);
|
||||
|
||||
const checkGlobalPromptShare = generateCheckAccess(
|
||||
PermissionTypes.PROMPTS,
|
||||
[Permissions.USE, Permissions.CREATE],
|
||||
|
||||
@@ -20,7 +20,10 @@ router.get('/:roleName', async (req, res) => {
|
||||
// TODO: TEMP, use a better parsing for roleName
|
||||
const roleName = _r.toUpperCase();
|
||||
|
||||
if (req.user.role !== SystemRoles.ADMIN && !roleDefaults[roleName]) {
|
||||
if (
|
||||
(req.user.role !== SystemRoles.ADMIN && roleName === SystemRoles.ADMIN) ||
|
||||
(req.user.role !== SystemRoles.ADMIN && !roleDefaults[roleName])
|
||||
) {
|
||||
return res.status(403).send({ message: 'Unauthorized' });
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,15 @@ const {
|
||||
verifyEmailController,
|
||||
updateUserPluginsController,
|
||||
resendVerificationController,
|
||||
getTermsStatusController,
|
||||
acceptTermsController,
|
||||
} = require('~/server/controllers/UserController');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', requireJwtAuth, getUserController);
|
||||
router.get('/terms', requireJwtAuth, getTermsStatusController);
|
||||
router.post('/terms/accept', requireJwtAuth, acceptTermsController);
|
||||
router.post('/plugins', requireJwtAuth, updateUserPluginsController);
|
||||
router.delete('/delete', requireJwtAuth, canDeleteAccount, deleteUserController);
|
||||
router.post('/verify', verifyEmailController);
|
||||
|
||||
@@ -6,6 +6,7 @@ const {
|
||||
isImageVisionTool,
|
||||
actionDomainSeparator,
|
||||
} = require('librechat-data-provider');
|
||||
const { tool } = require('@langchain/core/tools');
|
||||
const { encryptV2, decryptV2 } = require('~/server/utils/crypto');
|
||||
const { getActions, deleteActions } = require('~/models/Action');
|
||||
const { deleteAssistant } = require('~/models/Assistant');
|
||||
@@ -101,7 +102,8 @@ async function domainParser(req, domain, inverse = false) {
|
||||
*
|
||||
* @param {Object} searchParams - The parameters for loading action sets.
|
||||
* @param {string} searchParams.user - The user identifier.
|
||||
* @param {string} searchParams.assistant_id - The assistant identifier.
|
||||
* @param {string} [searchParams.agent_id]- The agent identifier.
|
||||
* @param {string} [searchParams.assistant_id]- The assistant identifier.
|
||||
* @returns {Promise<Action[] | null>} A promise that resolves to an array of actions or `null` if no match.
|
||||
*/
|
||||
async function loadActionSets(searchParams) {
|
||||
@@ -114,10 +116,14 @@ async function loadActionSets(searchParams) {
|
||||
* @param {Object} params - The parameters for loading action sets.
|
||||
* @param {Action} params.action - The action set. Necessary for decrypting authentication values.
|
||||
* @param {ActionRequest} params.requestBuilder - The ActionRequest builder class to execute the API call.
|
||||
* @returns { { _call: (toolInput: Object) => unknown} } An object with `_call` method to execute the tool input.
|
||||
* @param {string | undefined} [params.name] - The name of the tool.
|
||||
* @param {string | undefined} [params.description] - The description for the tool.
|
||||
* @param {import('zod').ZodTypeAny | undefined} [params.zodSchema] - The Zod schema for tool input validation/definition
|
||||
* @returns { Promsie<typeof tool | { _call: (toolInput: Object | string) => unknown}> } An object with `_call` method to execute the tool input.
|
||||
*/
|
||||
async function createActionTool({ action, requestBuilder }) {
|
||||
async function createActionTool({ action, requestBuilder, zodSchema, name, description }) {
|
||||
action.metadata = await decryptMetadata(action.metadata);
|
||||
/** @type {(toolInput: Object | string) => Promise<unknown>} */
|
||||
const _call = async (toolInput) => {
|
||||
try {
|
||||
requestBuilder.setParams(toolInput);
|
||||
@@ -142,6 +148,14 @@ async function createActionTool({ action, requestBuilder }) {
|
||||
}
|
||||
};
|
||||
|
||||
if (name) {
|
||||
return tool(_call, {
|
||||
name,
|
||||
description: description || '',
|
||||
schema: zodSchema,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
_call,
|
||||
};
|
||||
@@ -151,7 +165,7 @@ async function createActionTool({ action, requestBuilder }) {
|
||||
* Encrypts sensitive metadata values for an action.
|
||||
*
|
||||
* @param {ActionMetadata} metadata - The action metadata to encrypt.
|
||||
* @returns {ActionMetadata} The updated action metadata with encrypted values.
|
||||
* @returns {Promise<ActionMetadata>} The updated action metadata with encrypted values.
|
||||
*/
|
||||
async function encryptMetadata(metadata) {
|
||||
const encryptedMetadata = { ...metadata };
|
||||
@@ -180,7 +194,7 @@ async function encryptMetadata(metadata) {
|
||||
* Decrypts sensitive metadata values for an action.
|
||||
*
|
||||
* @param {ActionMetadata} metadata - The action metadata to decrypt.
|
||||
* @returns {ActionMetadata} The updated action metadata with decrypted values.
|
||||
* @returns {Promise<ActionMetadata>} The updated action metadata with decrypted values.
|
||||
*/
|
||||
async function decryptMetadata(metadata) {
|
||||
const decryptedMetadata = { ...metadata };
|
||||
|
||||
@@ -94,18 +94,19 @@ const AppService = async (app) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (endpoints?.[EModelEndpoint.openAI]) {
|
||||
endpointLocals[EModelEndpoint.openAI] = endpoints[EModelEndpoint.openAI];
|
||||
}
|
||||
if (endpoints?.[EModelEndpoint.google]) {
|
||||
endpointLocals[EModelEndpoint.google] = endpoints[EModelEndpoint.google];
|
||||
}
|
||||
if (endpoints?.[EModelEndpoint.anthropic]) {
|
||||
endpointLocals[EModelEndpoint.anthropic] = endpoints[EModelEndpoint.anthropic];
|
||||
}
|
||||
if (endpoints?.[EModelEndpoint.gptPlugins]) {
|
||||
endpointLocals[EModelEndpoint.gptPlugins] = endpoints[EModelEndpoint.gptPlugins];
|
||||
}
|
||||
const endpointKeys = [
|
||||
EModelEndpoint.openAI,
|
||||
EModelEndpoint.google,
|
||||
EModelEndpoint.bedrock,
|
||||
EModelEndpoint.anthropic,
|
||||
EModelEndpoint.gptPlugins,
|
||||
];
|
||||
|
||||
endpointKeys.forEach((key) => {
|
||||
if (endpoints?.[key]) {
|
||||
endpointLocals[key] = endpoints[key];
|
||||
}
|
||||
});
|
||||
|
||||
app.locals = {
|
||||
...defaultLocals,
|
||||
|
||||
@@ -45,5 +45,8 @@ module.exports = {
|
||||
AZURE_ASSISTANTS_BASE_URL,
|
||||
EModelEndpoint.azureAssistants,
|
||||
),
|
||||
[EModelEndpoint.bedrock]: generateConfig(process.env.BEDROCK_AWS_SECRET_ACCESS_KEY),
|
||||
/* key will be part of separate config */
|
||||
[EModelEndpoint.agents]: generateConfig(process.env.I_AM_A_TEAPOT),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,13 +9,13 @@ const { config } = require('./EndpointService');
|
||||
*/
|
||||
async function loadDefaultEndpointsConfig(req) {
|
||||
const { google, gptPlugins } = await loadAsyncEndpoints(req);
|
||||
const { openAI, assistants, azureAssistants, bingAI, anthropic, azureOpenAI, chatGPTBrowser } =
|
||||
config;
|
||||
const { assistants, azureAssistants, bingAI, azureOpenAI, chatGPTBrowser } = config;
|
||||
|
||||
const enabledEndpoints = getEnabledEndpoints();
|
||||
|
||||
const endpointConfig = {
|
||||
[EModelEndpoint.openAI]: openAI,
|
||||
[EModelEndpoint.openAI]: config[EModelEndpoint.openAI],
|
||||
[EModelEndpoint.agents]: config[EModelEndpoint.agents],
|
||||
[EModelEndpoint.assistants]: assistants,
|
||||
[EModelEndpoint.azureAssistants]: azureAssistants,
|
||||
[EModelEndpoint.azureOpenAI]: azureOpenAI,
|
||||
@@ -23,7 +23,8 @@ async function loadDefaultEndpointsConfig(req) {
|
||||
[EModelEndpoint.bingAI]: bingAI,
|
||||
[EModelEndpoint.chatGPTBrowser]: chatGPTBrowser,
|
||||
[EModelEndpoint.gptPlugins]: gptPlugins,
|
||||
[EModelEndpoint.anthropic]: anthropic,
|
||||
[EModelEndpoint.anthropic]: config[EModelEndpoint.anthropic],
|
||||
[EModelEndpoint.bedrock]: config[EModelEndpoint.bedrock],
|
||||
};
|
||||
|
||||
const orderedAndFilteredEndpoints = enabledEndpoints.reduce((config, key, index) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ const { useAzurePlugins } = require('~/server/services/Config/EndpointService').
|
||||
const {
|
||||
getOpenAIModels,
|
||||
getGoogleModels,
|
||||
getBedrockModels,
|
||||
getAnthropicModels,
|
||||
getChatGPTBrowserModels,
|
||||
} = require('~/server/services/ModelService');
|
||||
@@ -29,6 +30,7 @@ async function loadDefaultModels(req) {
|
||||
|
||||
return {
|
||||
[EModelEndpoint.openAI]: openAI,
|
||||
[EModelEndpoint.agents]: openAI,
|
||||
[EModelEndpoint.google]: google,
|
||||
[EModelEndpoint.anthropic]: anthropic,
|
||||
[EModelEndpoint.gptPlugins]: gptPlugins,
|
||||
@@ -37,6 +39,7 @@ async function loadDefaultModels(req) {
|
||||
[EModelEndpoint.chatGPTBrowser]: chatGPTBrowser,
|
||||
[EModelEndpoint.assistants]: assistants,
|
||||
[EModelEndpoint.azureAssistants]: azureAssistants,
|
||||
[EModelEndpoint.bedrock]: getBedrockModels(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
28
api/server/services/Endpoints/agents/build.js
Normal file
28
api/server/services/Endpoints/agents/build.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const { getAgent } = require('~/models/Agent');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const buildOptions = (req, endpoint, parsedBody) => {
|
||||
const { agent_id, instructions, spec, ...model_parameters } = parsedBody;
|
||||
|
||||
const agentPromise = getAgent({
|
||||
id: agent_id,
|
||||
// TODO: better author handling
|
||||
author: req.user.id,
|
||||
}).catch((error) => {
|
||||
logger.error(`[/agents/:${agent_id}] Error retrieving agent during build options step`, error);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const endpointOption = {
|
||||
agent: agentPromise,
|
||||
endpoint,
|
||||
agent_id,
|
||||
instructions,
|
||||
spec,
|
||||
model_parameters,
|
||||
};
|
||||
|
||||
return endpointOption;
|
||||
};
|
||||
|
||||
module.exports = { buildOptions };
|
||||
7
api/server/services/Endpoints/agents/index.js
Normal file
7
api/server/services/Endpoints/agents/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const build = require('./build');
|
||||
const initialize = require('./initialize');
|
||||
|
||||
module.exports = {
|
||||
...build,
|
||||
...initialize,
|
||||
};
|
||||
133
api/server/services/Endpoints/agents/initialize.js
Normal file
133
api/server/services/Endpoints/agents/initialize.js
Normal file
@@ -0,0 +1,133 @@
|
||||
// const {
|
||||
// ErrorTypes,
|
||||
// EModelEndpoint,
|
||||
// resolveHeaders,
|
||||
// mapModelToAzureConfig,
|
||||
// } = require('librechat-data-provider');
|
||||
// const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
// const { isEnabled, isUserProvided } = require('~/server/utils');
|
||||
// const { getAzureCredentials } = require('~/utils');
|
||||
// const { OpenAIClient } = require('~/app');
|
||||
|
||||
const { z } = require('zod');
|
||||
const { tool } = require('@langchain/core/tools');
|
||||
const { createContentAggregator } = require('@librechat/agents');
|
||||
const {
|
||||
EModelEndpoint,
|
||||
providerEndpointMap,
|
||||
getResponseSender,
|
||||
} = require('librechat-data-provider');
|
||||
const { getDefaultHandlers } = require('~/server/controllers/agents/callbacks');
|
||||
// for testing purposes
|
||||
// const createTavilySearchTool = require('~/app/clients/tools/structured/TavilySearch');
|
||||
const initAnthropic = require('~/server/services/Endpoints/anthropic/initializeClient');
|
||||
const initOpenAI = require('~/server/services/Endpoints/openAI/initializeClient');
|
||||
const { loadAgentTools } = require('~/server/services/ToolService');
|
||||
const AgentClient = require('~/server/controllers/agents/client');
|
||||
const { getModelMaxTokens } = require('~/utils');
|
||||
|
||||
/* For testing errors */
|
||||
const _getWeather = tool(
|
||||
async ({ location }) => {
|
||||
if (location === 'SAN FRANCISCO') {
|
||||
return 'It\'s 60 degrees and foggy';
|
||||
} else if (location.toLowerCase() === 'san francisco') {
|
||||
throw new Error('Input queries must be all capitals');
|
||||
} else {
|
||||
throw new Error('Invalid input.');
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'get_weather',
|
||||
description: 'Call to get the current weather',
|
||||
schema: z.object({
|
||||
location: z.string(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const providerConfigMap = {
|
||||
[EModelEndpoint.openAI]: initOpenAI,
|
||||
[EModelEndpoint.azureOpenAI]: initOpenAI,
|
||||
[EModelEndpoint.anthropic]: initAnthropic,
|
||||
};
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
if (!endpointOption) {
|
||||
throw new Error('Endpoint option not provided');
|
||||
}
|
||||
|
||||
// TODO: use endpointOption to determine options/modelOptions
|
||||
const { contentParts, aggregateContent } = createContentAggregator();
|
||||
const eventHandlers = getDefaultHandlers({ res, aggregateContent });
|
||||
|
||||
// const tools = [createTavilySearchTool()];
|
||||
// const tools = [_getWeather];
|
||||
// const tool_calls = [{ name: 'getPeople_action_swapi---dev' }];
|
||||
// const tool_calls = [{ name: 'dalle' }];
|
||||
// const tool_calls = [{ name: 'getItmOptions_action_YWlhcGkzLn' }];
|
||||
// const tool_calls = [{ name: 'tavily_search_results_json' }];
|
||||
// const tool_calls = [
|
||||
// { name: 'searchListings_action_emlsbG93NT' },
|
||||
// { name: 'searchAddress_action_emlsbG93NT' },
|
||||
// { name: 'searchMLS_action_emlsbG93NT' },
|
||||
// { name: 'searchCoordinates_action_emlsbG93NT' },
|
||||
// { name: 'searchUrl_action_emlsbG93NT' },
|
||||
// { name: 'getPropertyDetails_action_emlsbG93NT' },
|
||||
// ];
|
||||
|
||||
if (!endpointOption.agent) {
|
||||
throw new Error('No agent promise provided');
|
||||
}
|
||||
|
||||
/** @type {Agent} */
|
||||
const agent = await endpointOption.agent;
|
||||
const { tools, toolMap } = await loadAgentTools({
|
||||
req,
|
||||
tools: agent.tools,
|
||||
agent_id: agent.id,
|
||||
// openAIApiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
let modelOptions = { model: agent.model };
|
||||
const getOptions = providerConfigMap[agent.provider];
|
||||
if (!getOptions) {
|
||||
throw new Error(`Provider ${agent.provider} not supported`);
|
||||
}
|
||||
|
||||
// TODO: pass-in override settings that are specific to current run
|
||||
endpointOption.model_parameters.model = agent.model;
|
||||
const options = await getOptions({
|
||||
req,
|
||||
res,
|
||||
endpointOption,
|
||||
optionsOnly: true,
|
||||
overrideEndpoint: agent.provider,
|
||||
overrideModel: agent.model,
|
||||
});
|
||||
modelOptions = Object.assign(modelOptions, options.llmConfig);
|
||||
|
||||
const sender = getResponseSender({
|
||||
...endpointOption,
|
||||
model: endpointOption.model_parameters.model,
|
||||
});
|
||||
|
||||
const client = new AgentClient({
|
||||
req,
|
||||
agent,
|
||||
tools,
|
||||
sender,
|
||||
toolMap,
|
||||
contentParts,
|
||||
modelOptions,
|
||||
eventHandlers,
|
||||
endpoint: EModelEndpoint.agents,
|
||||
configOptions: options.configOptions,
|
||||
maxContextTokens:
|
||||
agent.max_context_tokens ??
|
||||
getModelMaxTokens(modelOptions.model, providerEndpointMap[agent.provider]),
|
||||
});
|
||||
return { client };
|
||||
};
|
||||
|
||||
module.exports = { initializeClient };
|
||||
@@ -23,7 +23,7 @@ const addTitle = async (req, { text, response, client }) => {
|
||||
|
||||
const title = await client.titleConvo({
|
||||
text,
|
||||
responseText: response?.text,
|
||||
responseText: response?.text ?? '',
|
||||
conversationId: response.conversationId,
|
||||
});
|
||||
await titleCache.set(key, title, 120000);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { getLLMConfig } = require('~/server/services/Endpoints/anthropic/llm');
|
||||
const { AnthropicClient } = require('~/app');
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
const initializeClient = async ({ req, res, endpointOption, optionsOnly }) => {
|
||||
const { ANTHROPIC_API_KEY, ANTHROPIC_REVERSE_PROXY, PROXY } = process.env;
|
||||
const expiresAt = req.body.key;
|
||||
const isUserProvided = ANTHROPIC_API_KEY === 'user_provided';
|
||||
@@ -34,6 +35,18 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
clientOptions.streamRate = allConfig.streamRate;
|
||||
}
|
||||
|
||||
if (optionsOnly) {
|
||||
const requestOptions = Object.assign(
|
||||
{
|
||||
reverseProxyUrl: ANTHROPIC_REVERSE_PROXY ?? null,
|
||||
proxy: PROXY ?? null,
|
||||
modelOptions: endpointOption.modelOptions,
|
||||
},
|
||||
clientOptions,
|
||||
);
|
||||
return getLLMConfig(anthropicApiKey, requestOptions);
|
||||
}
|
||||
|
||||
const client = new AnthropicClient(anthropicApiKey, {
|
||||
req,
|
||||
res,
|
||||
|
||||
55
api/server/services/Endpoints/anthropic/llm.js
Normal file
55
api/server/services/Endpoints/anthropic/llm.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { anthropicSettings, removeNullishValues } = require('librechat-data-provider');
|
||||
|
||||
/**
|
||||
* Generates configuration options for creating an Anthropic language model (LLM) instance.
|
||||
*
|
||||
* @param {string} apiKey - The API key for authentication with Anthropic.
|
||||
* @param {Object} [options={}] - Additional options for configuring the LLM.
|
||||
* @param {Object} [options.modelOptions] - Model-specific options.
|
||||
* @param {string} [options.modelOptions.model] - The name of the model to use.
|
||||
* @param {number} [options.modelOptions.maxOutputTokens] - The maximum number of tokens to generate.
|
||||
* @param {number} [options.modelOptions.temperature] - Controls randomness in output generation.
|
||||
* @param {number} [options.modelOptions.topP] - Controls diversity of output generation.
|
||||
* @param {number} [options.modelOptions.topK] - Controls the number of top tokens to consider.
|
||||
* @param {string[]} [options.modelOptions.stop] - Sequences where the API will stop generating further tokens.
|
||||
* @param {boolean} [options.modelOptions.stream] - Whether to stream the response.
|
||||
* @param {string} [options.proxy] - Proxy server URL.
|
||||
* @param {string} [options.reverseProxyUrl] - URL for a reverse proxy, if used.
|
||||
*
|
||||
* @returns {Object} Configuration options for creating an Anthropic LLM instance, with null and undefined values removed.
|
||||
*/
|
||||
function getLLMConfig(apiKey, options = {}) {
|
||||
const defaultOptions = {
|
||||
model: anthropicSettings.model.default,
|
||||
maxOutputTokens: anthropicSettings.maxOutputTokens.default,
|
||||
stream: true,
|
||||
};
|
||||
|
||||
const mergedOptions = Object.assign(defaultOptions, options.modelOptions);
|
||||
|
||||
const requestOptions = {
|
||||
apiKey,
|
||||
model: mergedOptions.model,
|
||||
stream: mergedOptions.stream,
|
||||
temperature: mergedOptions.temperature,
|
||||
top_p: mergedOptions.topP,
|
||||
top_k: mergedOptions.topK,
|
||||
stop_sequences: mergedOptions.stop,
|
||||
max_tokens:
|
||||
mergedOptions.maxOutputTokens || anthropicSettings.maxOutputTokens.reset(mergedOptions.model),
|
||||
};
|
||||
|
||||
const configOptions = {};
|
||||
if (options.proxy) {
|
||||
configOptions.httpAgent = new HttpsProxyAgent(options.proxy);
|
||||
}
|
||||
|
||||
if (options.reverseProxyUrl) {
|
||||
configOptions.baseURL = options.reverseProxyUrl;
|
||||
}
|
||||
|
||||
return { llmConfig: removeNullishValues(requestOptions), configOptions };
|
||||
}
|
||||
|
||||
module.exports = { getLLMConfig };
|
||||
44
api/server/services/Endpoints/bedrock/build.js
Normal file
44
api/server/services/Endpoints/bedrock/build.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const { removeNullishValues, bedrockInputParser } = require('librechat-data-provider');
|
||||
const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const buildOptions = (endpoint, parsedBody) => {
|
||||
const {
|
||||
modelLabel: name,
|
||||
promptPrefix,
|
||||
maxContextTokens,
|
||||
resendFiles = true,
|
||||
imageDetail,
|
||||
iconURL,
|
||||
greeting,
|
||||
spec,
|
||||
artifacts,
|
||||
...model_parameters
|
||||
} = parsedBody;
|
||||
let parsedParams = model_parameters;
|
||||
try {
|
||||
parsedParams = bedrockInputParser.parse(model_parameters);
|
||||
} catch (error) {
|
||||
logger.warn('Failed to parse bedrock input', error);
|
||||
}
|
||||
const endpointOption = removeNullishValues({
|
||||
endpoint,
|
||||
name,
|
||||
resendFiles,
|
||||
imageDetail,
|
||||
iconURL,
|
||||
greeting,
|
||||
spec,
|
||||
promptPrefix,
|
||||
maxContextTokens,
|
||||
model_parameters: parsedParams,
|
||||
});
|
||||
|
||||
if (typeof artifacts === 'string') {
|
||||
endpointOption.artifactsPrompt = generateArtifactsPrompt({ endpoint, artifacts });
|
||||
}
|
||||
|
||||
return endpointOption;
|
||||
};
|
||||
|
||||
module.exports = { buildOptions };
|
||||
7
api/server/services/Endpoints/bedrock/index.js
Normal file
7
api/server/services/Endpoints/bedrock/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const build = require('./build');
|
||||
const initialize = require('./initialize');
|
||||
|
||||
module.exports = {
|
||||
...build,
|
||||
...initialize,
|
||||
};
|
||||
76
api/server/services/Endpoints/bedrock/initialize.js
Normal file
76
api/server/services/Endpoints/bedrock/initialize.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const { createContentAggregator } = require('@librechat/agents');
|
||||
const {
|
||||
EModelEndpoint,
|
||||
providerEndpointMap,
|
||||
getResponseSender,
|
||||
} = require('librechat-data-provider');
|
||||
const { getDefaultHandlers } = require('~/server/controllers/agents/callbacks');
|
||||
// const { loadAgentTools } = require('~/server/services/ToolService');
|
||||
const getOptions = require('~/server/services/Endpoints/bedrock/options');
|
||||
const AgentClient = require('~/server/controllers/agents/client');
|
||||
const { getModelMaxTokens } = require('~/utils');
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
if (!endpointOption) {
|
||||
throw new Error('Endpoint option not provided');
|
||||
}
|
||||
|
||||
/** @type {Array<UsageMetadata>} */
|
||||
const collectedUsage = [];
|
||||
const { contentParts, aggregateContent } = createContentAggregator();
|
||||
const eventHandlers = getDefaultHandlers({ res, aggregateContent, collectedUsage });
|
||||
|
||||
// const tools = [createTavilySearchTool()];
|
||||
|
||||
/** @type {Agent} */
|
||||
const agent = {
|
||||
id: EModelEndpoint.bedrock,
|
||||
name: endpointOption.name,
|
||||
instructions: endpointOption.promptPrefix,
|
||||
provider: EModelEndpoint.bedrock,
|
||||
model: endpointOption.model_parameters.model,
|
||||
model_parameters: endpointOption.model_parameters,
|
||||
};
|
||||
|
||||
if (typeof endpointOption.artifactsPrompt === 'string' && endpointOption.artifactsPrompt) {
|
||||
agent.instructions = `${agent.instructions ?? ''}\n${endpointOption.artifactsPrompt}`.trim();
|
||||
}
|
||||
|
||||
let modelOptions = { model: agent.model };
|
||||
|
||||
// TODO: pass-in override settings that are specific to current run
|
||||
const options = await getOptions({
|
||||
req,
|
||||
res,
|
||||
endpointOption,
|
||||
});
|
||||
|
||||
modelOptions = Object.assign(modelOptions, options.llmConfig);
|
||||
const maxContextTokens =
|
||||
agent.max_context_tokens ??
|
||||
getModelMaxTokens(modelOptions.model, providerEndpointMap[agent.provider]);
|
||||
|
||||
const sender = getResponseSender({
|
||||
...endpointOption,
|
||||
model: endpointOption.model_parameters.model,
|
||||
});
|
||||
|
||||
const client = new AgentClient({
|
||||
req,
|
||||
agent,
|
||||
sender,
|
||||
// tools,
|
||||
// toolMap,
|
||||
modelOptions,
|
||||
contentParts,
|
||||
eventHandlers,
|
||||
collectedUsage,
|
||||
maxContextTokens,
|
||||
endpoint: EModelEndpoint.bedrock,
|
||||
configOptions: options.configOptions,
|
||||
attachments: endpointOption.attachments,
|
||||
});
|
||||
return { client };
|
||||
};
|
||||
|
||||
module.exports = { initializeClient };
|
||||
90
api/server/services/Endpoints/bedrock/options.js
Normal file
90
api/server/services/Endpoints/bedrock/options.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const {
|
||||
EModelEndpoint,
|
||||
Constants,
|
||||
AuthType,
|
||||
removeNullishValues,
|
||||
} = require('librechat-data-provider');
|
||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { sleep } = require('~/server/utils');
|
||||
|
||||
const getOptions = async ({ req, endpointOption }) => {
|
||||
const {
|
||||
BEDROCK_AWS_SECRET_ACCESS_KEY,
|
||||
BEDROCK_AWS_ACCESS_KEY_ID,
|
||||
BEDROCK_REVERSE_PROXY,
|
||||
BEDROCK_AWS_DEFAULT_REGION,
|
||||
PROXY,
|
||||
} = process.env;
|
||||
const expiresAt = req.body.key;
|
||||
const isUserProvided = BEDROCK_AWS_SECRET_ACCESS_KEY === AuthType.USER_PROVIDED;
|
||||
|
||||
const credentials = isUserProvided
|
||||
? await getUserKey({ userId: req.user.id, name: EModelEndpoint.bedrock })
|
||||
: {
|
||||
accessKeyId: BEDROCK_AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: BEDROCK_AWS_SECRET_ACCESS_KEY,
|
||||
};
|
||||
|
||||
if (!credentials) {
|
||||
throw new Error('Bedrock credentials not provided. Please provide them again.');
|
||||
}
|
||||
|
||||
if (expiresAt && isUserProvided) {
|
||||
checkUserKeyExpiry(expiresAt, EModelEndpoint.bedrock);
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
let streamRate = Constants.DEFAULT_STREAM_RATE;
|
||||
|
||||
/** @type {undefined | TBaseEndpoint} */
|
||||
const bedrockConfig = req.app.locals[EModelEndpoint.bedrock];
|
||||
|
||||
if (bedrockConfig && bedrockConfig.streamRate) {
|
||||
streamRate = bedrockConfig.streamRate;
|
||||
}
|
||||
|
||||
/** @type {undefined | TBaseEndpoint} */
|
||||
const allConfig = req.app.locals.all;
|
||||
if (allConfig && allConfig.streamRate) {
|
||||
streamRate = allConfig.streamRate;
|
||||
}
|
||||
|
||||
/** @type {import('@librechat/agents').BedrockConverseClientOptions} */
|
||||
const requestOptions = Object.assign(
|
||||
{
|
||||
credentials,
|
||||
model: endpointOption.model,
|
||||
region: BEDROCK_AWS_DEFAULT_REGION,
|
||||
streaming: true,
|
||||
streamUsage: true,
|
||||
callbacks: [
|
||||
{
|
||||
handleLLMNewToken: async () => {
|
||||
if (!streamRate) {
|
||||
return;
|
||||
}
|
||||
await sleep(streamRate);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
endpointOption.model_parameters,
|
||||
);
|
||||
|
||||
const configOptions = {};
|
||||
if (PROXY) {
|
||||
configOptions.httpAgent = new HttpsProxyAgent(PROXY);
|
||||
}
|
||||
|
||||
if (BEDROCK_REVERSE_PROXY) {
|
||||
configOptions.endpointHost = BEDROCK_REVERSE_PROXY;
|
||||
}
|
||||
|
||||
return {
|
||||
llmConfig: removeNullishValues(requestOptions),
|
||||
configOptions,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = getOptions;
|
||||
40
api/server/services/Endpoints/bedrock/title.js
Normal file
40
api/server/services/Endpoints/bedrock/title.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { CacheKeys } = require('librechat-data-provider');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { saveConvo } = require('~/models');
|
||||
|
||||
const addTitle = async (req, { text, response, client }) => {
|
||||
const { TITLE_CONVO = true } = process.env ?? {};
|
||||
if (!isEnabled(TITLE_CONVO)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client.options.titleConvo === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the request was aborted, don't generate the title.
|
||||
if (client.abortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const titleCache = getLogStores(CacheKeys.GEN_TITLE);
|
||||
const key = `${req.user.id}-${response.conversationId}`;
|
||||
|
||||
const title = await client.titleConvo({
|
||||
text,
|
||||
responseText: response?.text ?? '',
|
||||
conversationId: response.conversationId,
|
||||
});
|
||||
await titleCache.set(key, title, 120000);
|
||||
await saveConvo(
|
||||
req,
|
||||
{
|
||||
conversationId: response.conversationId,
|
||||
title,
|
||||
},
|
||||
{ context: 'api/server/services/Endpoints/bedrock/title.js' },
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = addTitle;
|
||||
@@ -49,7 +49,7 @@ const addTitle = async (req, { text, response, client }) => {
|
||||
|
||||
const title = await titleClient.titleConvo({
|
||||
text,
|
||||
responseText: response?.text,
|
||||
responseText: response?.text ?? '',
|
||||
conversationId: response.conversationId,
|
||||
});
|
||||
await titleCache.set(key, title, 120000);
|
||||
|
||||
@@ -6,7 +6,7 @@ const buildOptions = (endpoint, parsedBody) => {
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
agentOptions,
|
||||
tools,
|
||||
tools = [],
|
||||
iconURL,
|
||||
greeting,
|
||||
spec,
|
||||
@@ -19,7 +19,7 @@ const buildOptions = (endpoint, parsedBody) => {
|
||||
tools:
|
||||
tools
|
||||
.map((tool) => tool?.pluginKey ?? tool)
|
||||
.filter((toolName) => typeof toolName === 'string') ?? [],
|
||||
.filter((toolName) => typeof toolName === 'string'),
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
agentOptions,
|
||||
|
||||
@@ -23,7 +23,7 @@ const addTitle = async (req, { text, response, client }) => {
|
||||
|
||||
const title = await client.titleConvo({
|
||||
text,
|
||||
responseText: response?.text,
|
||||
responseText: response?.text ?? '',
|
||||
conversationId: response.conversationId,
|
||||
});
|
||||
await titleCache.set(key, title, 120000);
|
||||
|
||||
@@ -5,11 +5,19 @@ const {
|
||||
mapModelToAzureConfig,
|
||||
} = require('librechat-data-provider');
|
||||
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||
const { getLLMConfig } = require('~/server/services/Endpoints/openAI/llm');
|
||||
const { isEnabled, isUserProvided } = require('~/server/utils');
|
||||
const { getAzureCredentials } = require('~/utils');
|
||||
const { OpenAIClient } = require('~/app');
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
const initializeClient = async ({
|
||||
req,
|
||||
res,
|
||||
endpointOption,
|
||||
optionsOnly,
|
||||
overrideEndpoint,
|
||||
overrideModel,
|
||||
}) => {
|
||||
const {
|
||||
PROXY,
|
||||
OPENAI_API_KEY,
|
||||
@@ -19,7 +27,9 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
OPENAI_SUMMARIZE,
|
||||
DEBUG_OPENAI,
|
||||
} = process.env;
|
||||
const { key: expiresAt, endpoint, model: modelName } = req.body;
|
||||
const { key: expiresAt } = req.body;
|
||||
const modelName = overrideModel ?? req.body.model;
|
||||
const endpoint = overrideEndpoint ?? req.body.endpoint;
|
||||
const contextStrategy = isEnabled(OPENAI_SUMMARIZE) ? 'summarize' : null;
|
||||
|
||||
const credentials = {
|
||||
@@ -45,12 +55,10 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
let baseURL = userProvidesURL ? userValues?.baseURL : baseURLOptions[endpoint];
|
||||
|
||||
const clientOptions = {
|
||||
debug: isEnabled(DEBUG_OPENAI),
|
||||
contextStrategy,
|
||||
reverseProxyUrl: baseURL ? baseURL : null,
|
||||
proxy: PROXY ?? null,
|
||||
req,
|
||||
res,
|
||||
debug: isEnabled(DEBUG_OPENAI),
|
||||
reverseProxyUrl: baseURL ? baseURL : null,
|
||||
...endpointOption,
|
||||
};
|
||||
|
||||
@@ -119,7 +127,17 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
throw new Error(`${endpoint} API Key not provided.`);
|
||||
}
|
||||
|
||||
const client = new OpenAIClient(apiKey, clientOptions);
|
||||
if (optionsOnly) {
|
||||
const requestOptions = Object.assign(
|
||||
{
|
||||
modelOptions: endpointOption.modelOptions,
|
||||
},
|
||||
clientOptions,
|
||||
);
|
||||
return getLLMConfig(apiKey, requestOptions);
|
||||
}
|
||||
|
||||
const client = new OpenAIClient(apiKey, Object.assign({ req, res }, clientOptions));
|
||||
return {
|
||||
client,
|
||||
openAIApiKey: apiKey,
|
||||
|
||||
120
api/server/services/Endpoints/openAI/llm.js
Normal file
120
api/server/services/Endpoints/openAI/llm.js
Normal file
@@ -0,0 +1,120 @@
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { sanitizeModelName, constructAzureURL } = require('~/utils');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
|
||||
/**
|
||||
* Generates configuration options for creating a language model (LLM) instance.
|
||||
* @param {string} apiKey - The API key for authentication.
|
||||
* @param {Object} options - Additional options for configuring the LLM.
|
||||
* @param {Object} [options.modelOptions] - Model-specific options.
|
||||
* @param {string} [options.modelOptions.model] - The name of the model to use.
|
||||
* @param {number} [options.modelOptions.temperature] - Controls randomness in output generation (0-2).
|
||||
* @param {number} [options.modelOptions.top_p] - Controls diversity via nucleus sampling (0-1).
|
||||
* @param {number} [options.modelOptions.frequency_penalty] - Reduces repetition of token sequences (-2 to 2).
|
||||
* @param {number} [options.modelOptions.presence_penalty] - Encourages discussing new topics (-2 to 2).
|
||||
* @param {number} [options.modelOptions.max_tokens] - The maximum number of tokens to generate.
|
||||
* @param {string[]} [options.modelOptions.stop] - Sequences where the API will stop generating further tokens.
|
||||
* @param {string} [options.reverseProxyUrl] - URL for a reverse proxy, if used.
|
||||
* @param {boolean} [options.useOpenRouter] - Flag to use OpenRouter API.
|
||||
* @param {Object} [options.headers] - Additional headers for API requests.
|
||||
* @param {string} [options.proxy] - Proxy server URL.
|
||||
* @param {Object} [options.azure] - Azure-specific configurations.
|
||||
* @param {boolean} [options.streaming] - Whether to use streaming mode.
|
||||
* @param {Object} [options.addParams] - Additional parameters to add to the model options.
|
||||
* @param {string[]} [options.dropParams] - Parameters to remove from the model options.
|
||||
* @returns {Object} Configuration options for creating an LLM instance.
|
||||
*/
|
||||
function getLLMConfig(apiKey, options = {}) {
|
||||
const {
|
||||
modelOptions = {},
|
||||
reverseProxyUrl,
|
||||
useOpenRouter,
|
||||
headers,
|
||||
proxy,
|
||||
azure,
|
||||
streaming = true,
|
||||
addParams,
|
||||
dropParams,
|
||||
} = options;
|
||||
|
||||
let llmConfig = {
|
||||
model: 'gpt-4o-mini',
|
||||
streaming,
|
||||
};
|
||||
|
||||
Object.assign(llmConfig, modelOptions);
|
||||
|
||||
if (addParams && typeof addParams === 'object') {
|
||||
Object.assign(llmConfig, addParams);
|
||||
}
|
||||
|
||||
if (dropParams && Array.isArray(dropParams)) {
|
||||
dropParams.forEach((param) => {
|
||||
delete llmConfig[param];
|
||||
});
|
||||
}
|
||||
|
||||
const configOptions = {};
|
||||
|
||||
// Handle OpenRouter or custom reverse proxy
|
||||
if (useOpenRouter || reverseProxyUrl === 'https://openrouter.ai/api/v1') {
|
||||
configOptions.basePath = 'https://openrouter.ai/api/v1';
|
||||
configOptions.baseOptions = {
|
||||
headers: Object.assign(
|
||||
{
|
||||
'HTTP-Referer': 'https://librechat.ai',
|
||||
'X-Title': 'LibreChat',
|
||||
},
|
||||
headers,
|
||||
),
|
||||
};
|
||||
} else if (reverseProxyUrl) {
|
||||
configOptions.basePath = reverseProxyUrl;
|
||||
if (headers) {
|
||||
configOptions.baseOptions = { headers };
|
||||
}
|
||||
}
|
||||
|
||||
if (proxy) {
|
||||
const proxyAgent = new HttpsProxyAgent(proxy);
|
||||
Object.assign(configOptions, {
|
||||
httpAgent: proxyAgent,
|
||||
httpsAgent: proxyAgent,
|
||||
});
|
||||
}
|
||||
|
||||
if (azure) {
|
||||
const useModelName = isEnabled(process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME);
|
||||
azure.azureOpenAIApiDeploymentName = useModelName
|
||||
? sanitizeModelName(llmConfig.model)
|
||||
: azure.azureOpenAIApiDeploymentName;
|
||||
|
||||
if (process.env.AZURE_OPENAI_DEFAULT_MODEL) {
|
||||
llmConfig.model = process.env.AZURE_OPENAI_DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
if (configOptions.basePath) {
|
||||
const azureURL = constructAzureURL({
|
||||
baseURL: configOptions.basePath,
|
||||
azureOptions: azure,
|
||||
});
|
||||
azure.azureOpenAIBasePath = azureURL.split(`/${azure.azureOpenAIApiDeploymentName}`)[0];
|
||||
}
|
||||
|
||||
Object.assign(llmConfig, azure);
|
||||
llmConfig.model = llmConfig.azureOpenAIApiDeploymentName;
|
||||
} else {
|
||||
llmConfig.openAIApiKey = apiKey;
|
||||
// Object.assign(llmConfig, {
|
||||
// configuration: { apiKey },
|
||||
// });
|
||||
}
|
||||
|
||||
if (process.env.OPENAI_ORGANIZATION && this.azure) {
|
||||
llmConfig.organization = process.env.OPENAI_ORGANIZATION;
|
||||
}
|
||||
|
||||
return { llmConfig, configOptions };
|
||||
}
|
||||
|
||||
module.exports = { getLLMConfig };
|
||||
@@ -1,5 +1,6 @@
|
||||
const { Readable } = require('stream');
|
||||
const axios = require('axios');
|
||||
const FormData = require('form-data');
|
||||
const { Readable } = require('stream');
|
||||
const { extractEnvVariable, STTProviders } = require('librechat-data-provider');
|
||||
const getCustomConfig = require('~/server/services/Config/getCustomConfig');
|
||||
const { genAzureEndpoint } = require('~/utils');
|
||||
@@ -136,8 +137,10 @@ class STTService {
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
const audioBlob = new Blob([audioBuffer], { type: audioFile.mimetype });
|
||||
formData.append('file', audioBlob, audioFile.originalname);
|
||||
formData.append('file', audioBuffer, {
|
||||
filename: audioFile.originalname,
|
||||
contentType: audioFile.mimetype,
|
||||
});
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
@@ -146,7 +149,7 @@ class STTService {
|
||||
|
||||
[headers].forEach(this.removeUndefined);
|
||||
|
||||
return [url, formData, headers];
|
||||
return [url, formData, { ...headers, ...formData.getHeaders() }];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,11 +174,6 @@ class STTService {
|
||||
|
||||
const [url, data, headers] = strategy.call(this, sttSchema, audioReadStream, audioFile);
|
||||
|
||||
if (!Readable.from && data instanceof FormData) {
|
||||
const audioBlob = new Blob([audioBuffer], { type: audioFile.mimetype });
|
||||
data.set('file', audioBlob, audioFile.originalname);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(url, data, { headers });
|
||||
|
||||
|
||||
@@ -23,7 +23,13 @@ async function fetchImageToBase64(url) {
|
||||
}
|
||||
}
|
||||
|
||||
const base64Only = new Set([EModelEndpoint.google, EModelEndpoint.anthropic, 'Ollama', 'ollama']);
|
||||
const base64Only = new Set([
|
||||
EModelEndpoint.google,
|
||||
EModelEndpoint.anthropic,
|
||||
'Ollama',
|
||||
'ollama',
|
||||
EModelEndpoint.bedrock,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Encodes and formats the given files.
|
||||
|
||||
@@ -5,6 +5,21 @@ const { extractBaseURL, inputSchema, processModelData, logAxiosError } = require
|
||||
const { OllamaClient } = require('~/app/clients/OllamaClient');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
|
||||
/**
|
||||
* Splits a string by commas and trims each resulting value.
|
||||
* @param {string} input - The input string to split.
|
||||
* @returns {string[]} An array of trimmed values.
|
||||
*/
|
||||
const splitAndTrim = (input) => {
|
||||
if (!input || typeof input !== 'string') {
|
||||
return [];
|
||||
}
|
||||
return input
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const { openAIApiKey, userProvidedOpenAI } = require('./Config/EndpointService').config;
|
||||
|
||||
/**
|
||||
@@ -194,7 +209,7 @@ const getOpenAIModels = async (opts) => {
|
||||
}
|
||||
|
||||
if (process.env[key]) {
|
||||
models = String(process.env[key]).split(',');
|
||||
models = splitAndTrim(process.env[key]);
|
||||
return models;
|
||||
}
|
||||
|
||||
@@ -208,7 +223,7 @@ const getOpenAIModels = async (opts) => {
|
||||
const getChatGPTBrowserModels = () => {
|
||||
let models = ['text-davinci-002-render-sha', 'gpt-4'];
|
||||
if (process.env.CHATGPT_MODELS) {
|
||||
models = String(process.env.CHATGPT_MODELS).split(',');
|
||||
models = splitAndTrim(process.env.CHATGPT_MODELS);
|
||||
}
|
||||
|
||||
return models;
|
||||
@@ -217,7 +232,7 @@ const getChatGPTBrowserModels = () => {
|
||||
const getAnthropicModels = () => {
|
||||
let models = defaultModels[EModelEndpoint.anthropic];
|
||||
if (process.env.ANTHROPIC_MODELS) {
|
||||
models = String(process.env.ANTHROPIC_MODELS).split(',');
|
||||
models = splitAndTrim(process.env.ANTHROPIC_MODELS);
|
||||
}
|
||||
|
||||
return models;
|
||||
@@ -226,7 +241,16 @@ const getAnthropicModels = () => {
|
||||
const getGoogleModels = () => {
|
||||
let models = defaultModels[EModelEndpoint.google];
|
||||
if (process.env.GOOGLE_MODELS) {
|
||||
models = String(process.env.GOOGLE_MODELS).split(',');
|
||||
models = splitAndTrim(process.env.GOOGLE_MODELS);
|
||||
}
|
||||
|
||||
return models;
|
||||
};
|
||||
|
||||
const getBedrockModels = () => {
|
||||
let models = defaultModels[EModelEndpoint.bedrock];
|
||||
if (process.env.BEDROCK_AWS_MODELS) {
|
||||
models = splitAndTrim(process.env.BEDROCK_AWS_MODELS);
|
||||
}
|
||||
|
||||
return models;
|
||||
@@ -234,7 +258,9 @@ const getGoogleModels = () => {
|
||||
|
||||
module.exports = {
|
||||
fetchModels,
|
||||
splitAndTrim,
|
||||
getOpenAIModels,
|
||||
getBedrockModels,
|
||||
getChatGPTBrowserModels,
|
||||
getAnthropicModels,
|
||||
getGoogleModels,
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
const axios = require('axios');
|
||||
const { EModelEndpoint, defaultModels } = require('librechat-data-provider');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const { fetchModels, getOpenAIModels } = require('./ModelService');
|
||||
const {
|
||||
fetchModels,
|
||||
splitAndTrim,
|
||||
getOpenAIModels,
|
||||
getGoogleModels,
|
||||
getBedrockModels,
|
||||
getAnthropicModels,
|
||||
} = require('./ModelService');
|
||||
|
||||
jest.mock('~/utils', () => {
|
||||
const originalUtils = jest.requireActual('~/utils');
|
||||
return {
|
||||
@@ -329,3 +338,71 @@ describe('fetchModels with Ollama specific logic', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitAndTrim', () => {
|
||||
it('should split a string by commas and trim each value', () => {
|
||||
const input = ' model1, model2 , model3,model4 ';
|
||||
const expected = ['model1', 'model2', 'model3', 'model4'];
|
||||
expect(splitAndTrim(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return an empty array for empty input', () => {
|
||||
expect(splitAndTrim('')).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array for null input', () => {
|
||||
expect(splitAndTrim(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array for undefined input', () => {
|
||||
expect(splitAndTrim(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out empty values after trimming', () => {
|
||||
const input = 'model1,, ,model2,';
|
||||
const expected = ['model1', 'model2'];
|
||||
expect(splitAndTrim(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAnthropicModels', () => {
|
||||
it('returns default models when ANTHROPIC_MODELS is not set', () => {
|
||||
delete process.env.ANTHROPIC_MODELS;
|
||||
const models = getAnthropicModels();
|
||||
expect(models).toEqual(defaultModels[EModelEndpoint.anthropic]);
|
||||
});
|
||||
|
||||
it('returns models from ANTHROPIC_MODELS when set', () => {
|
||||
process.env.ANTHROPIC_MODELS = 'claude-1, claude-2 ';
|
||||
const models = getAnthropicModels();
|
||||
expect(models).toEqual(['claude-1', 'claude-2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGoogleModels', () => {
|
||||
it('returns default models when GOOGLE_MODELS is not set', () => {
|
||||
delete process.env.GOOGLE_MODELS;
|
||||
const models = getGoogleModels();
|
||||
expect(models).toEqual(defaultModels[EModelEndpoint.google]);
|
||||
});
|
||||
|
||||
it('returns models from GOOGLE_MODELS when set', () => {
|
||||
process.env.GOOGLE_MODELS = 'gemini-pro, bard ';
|
||||
const models = getGoogleModels();
|
||||
expect(models).toEqual(['gemini-pro', 'bard']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBedrockModels', () => {
|
||||
it('returns default models when BEDROCK_AWS_MODELS is not set', () => {
|
||||
delete process.env.BEDROCK_AWS_MODELS;
|
||||
const models = getBedrockModels();
|
||||
expect(models).toEqual(defaultModels[EModelEndpoint.bedrock]);
|
||||
});
|
||||
|
||||
it('returns models from BEDROCK_AWS_MODELS when set', () => {
|
||||
process.env.BEDROCK_AWS_MODELS = 'anthropic.claude-v2, ai21.j2-ultra ';
|
||||
const models = getBedrockModels();
|
||||
expect(models).toEqual(['anthropic.claude-v2', 'ai21.j2-ultra']);
|
||||
});
|
||||
});
|
||||
|
||||
64
api/server/services/Tokenizer.js
Normal file
64
api/server/services/Tokenizer.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
class Tokenizer {
|
||||
constructor() {
|
||||
this.tokenizersCache = {};
|
||||
this.tokenizerCallsCount = 0;
|
||||
}
|
||||
|
||||
getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
||||
let tokenizer;
|
||||
if (this.tokenizersCache[encoding]) {
|
||||
tokenizer = this.tokenizersCache[encoding];
|
||||
} else {
|
||||
if (isModelName) {
|
||||
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
||||
} else {
|
||||
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
||||
}
|
||||
this.tokenizersCache[encoding] = tokenizer;
|
||||
}
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
freeAndResetAllEncoders() {
|
||||
try {
|
||||
Object.keys(this.tokenizersCache).forEach((key) => {
|
||||
if (this.tokenizersCache[key]) {
|
||||
this.tokenizersCache[key].free();
|
||||
delete this.tokenizersCache[key];
|
||||
}
|
||||
});
|
||||
this.tokenizerCallsCount = 1;
|
||||
} catch (error) {
|
||||
logger.error('[Tokenizer] Free and reset encoders error', error);
|
||||
}
|
||||
}
|
||||
|
||||
resetTokenizersIfNecessary() {
|
||||
if (this.tokenizerCallsCount >= 25) {
|
||||
if (this.options?.debug) {
|
||||
logger.debug('[Tokenizer] freeAndResetAllEncoders: reached 25 encodings, resetting...');
|
||||
}
|
||||
this.freeAndResetAllEncoders();
|
||||
}
|
||||
this.tokenizerCallsCount++;
|
||||
}
|
||||
|
||||
getTokenCount(text, encoding = 'cl100k_base') {
|
||||
this.resetTokenizersIfNecessary();
|
||||
try {
|
||||
const tokenizer = this.getTokenizer(encoding);
|
||||
return tokenizer.encode(text, 'all').length;
|
||||
} catch (error) {
|
||||
this.freeAndResetAllEncoders();
|
||||
const tokenizer = this.getTokenizer(encoding);
|
||||
return tokenizer.encode(text, 'all').length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tokenizerService = new Tokenizer();
|
||||
|
||||
module.exports = tokenizerService;
|
||||
@@ -1,6 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { StructuredTool } = require('langchain/tools');
|
||||
const { tool: toolFn } = require('@langchain/core/tools');
|
||||
const { zodToJsonSchema } = require('zod-to-json-schema');
|
||||
const { Calculator } = require('langchain/tools/calculator');
|
||||
const {
|
||||
@@ -180,7 +181,7 @@ async function processRequiredActions(client, requiredActions) {
|
||||
const tools = requiredActions.map((action) => action.tool);
|
||||
const loadedTools = await loadTools({
|
||||
user: client.req.user.id,
|
||||
model: client.req.body.model ?? 'gpt-3.5-turbo-1106',
|
||||
model: client.req.body.model ?? 'gpt-4o-mini',
|
||||
tools,
|
||||
functions: true,
|
||||
options: {
|
||||
@@ -372,8 +373,120 @@ async function processRequiredActions(client, requiredActions) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the runtime tool calls and returns a combined toolMap.
|
||||
* @param {Object} params - Run params containing user and request information.
|
||||
* @param {ServerRequest} params.req - The request object.
|
||||
* @param {string} params.agent_id - The agent ID.
|
||||
* @param {string[]} params.tools - The agent's available tools.
|
||||
* @param {string | undefined} [params.openAIApiKey] - The OpenAI API key.
|
||||
* @returns {Promise<{ tools?: StructuredTool[]; toolMap?: Record<string, StructuredTool>}>} The combined toolMap.
|
||||
*/
|
||||
async function loadAgentTools({ req, agent_id, tools, openAIApiKey }) {
|
||||
if (!tools || tools.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const loadedTools = await loadTools({
|
||||
user: req.user.id,
|
||||
// model: req.body.model ?? 'gpt-4o-mini',
|
||||
tools,
|
||||
functions: true,
|
||||
options: {
|
||||
req,
|
||||
openAIApiKey,
|
||||
returnMetadata: true,
|
||||
processFileURL,
|
||||
uploadImageBuffer,
|
||||
fileStrategy: req.app.locals.fileStrategy,
|
||||
},
|
||||
skipSpecs: true,
|
||||
});
|
||||
|
||||
const agentTools = [];
|
||||
for (let i = 0; i < loadedTools.length; i++) {
|
||||
const tool = loadedTools[i];
|
||||
|
||||
const toolInstance = toolFn(
|
||||
async (...args) => {
|
||||
return tool['_call'](...args);
|
||||
},
|
||||
{
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
schema: tool.schema,
|
||||
},
|
||||
);
|
||||
|
||||
agentTools.push(toolInstance);
|
||||
}
|
||||
|
||||
const ToolMap = loadedTools.reduce((map, tool) => {
|
||||
map[tool.name] = tool;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
let actionSets = [];
|
||||
const ActionToolMap = {};
|
||||
|
||||
for (const toolName of tools) {
|
||||
if (!ToolMap[toolName]) {
|
||||
if (!actionSets.length) {
|
||||
actionSets = (await loadActionSets({ agent_id })) ?? [];
|
||||
}
|
||||
|
||||
let actionSet = null;
|
||||
let currentDomain = '';
|
||||
for (let action of actionSets) {
|
||||
const domain = await domainParser(req, action.metadata.domain, true);
|
||||
if (toolName.includes(domain)) {
|
||||
currentDomain = domain;
|
||||
actionSet = action;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (actionSet) {
|
||||
const validationResult = validateAndParseOpenAPISpec(actionSet.metadata.raw_spec);
|
||||
if (validationResult.spec) {
|
||||
const { requestBuilders, functionSignatures, zodSchemas } = openapiToFunction(
|
||||
validationResult.spec,
|
||||
true,
|
||||
);
|
||||
const functionName = toolName.replace(`${actionDelimiter}${currentDomain}`, '');
|
||||
const functionSig = functionSignatures.find((sig) => sig.name === functionName);
|
||||
const requestBuilder = requestBuilders[functionName];
|
||||
const zodSchema = zodSchemas[functionName];
|
||||
|
||||
if (requestBuilder) {
|
||||
const tool = await createActionTool({
|
||||
action: actionSet,
|
||||
requestBuilder,
|
||||
zodSchema,
|
||||
name: toolName,
|
||||
description: functionSig.description,
|
||||
});
|
||||
agentTools.push(tool);
|
||||
ActionToolMap[toolName] = tool;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tools.length > 0 && agentTools.length === 0) {
|
||||
throw new Error('No tools found for the specified tool calls.');
|
||||
}
|
||||
|
||||
const toolMap = { ...ToolMap, ...ActionToolMap };
|
||||
return {
|
||||
tools: agentTools,
|
||||
toolMap,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatToOpenAIAssistantTool,
|
||||
loadAgentTools,
|
||||
loadAndFormatTools,
|
||||
processRequiredActions,
|
||||
formatToOpenAIAssistantTool,
|
||||
};
|
||||
|
||||
@@ -31,11 +31,18 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
|
||||
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
|
||||
bookmarks: interfaceConfig?.bookmarks ?? defaults.bookmarks,
|
||||
prompts: interfaceConfig?.prompts ?? defaults.prompts,
|
||||
multiConvo: interfaceConfig?.multiConvo ?? defaults.multiConvo,
|
||||
});
|
||||
|
||||
await updateAccessPermissions(roleName, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo },
|
||||
});
|
||||
await updateAccessPermissions(SystemRoles.ADMIN, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo },
|
||||
});
|
||||
|
||||
let i = 0;
|
||||
|
||||
@@ -16,6 +16,7 @@ describe('loadDefaultInterface', () => {
|
||||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +29,7 @@ describe('loadDefaultInterface', () => {
|
||||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +42,7 @@ describe('loadDefaultInterface', () => {
|
||||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +55,7 @@ describe('loadDefaultInterface', () => {
|
||||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,6 +68,7 @@ describe('loadDefaultInterface', () => {
|
||||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,6 +81,72 @@ describe('loadDefaultInterface', () => {
|
||||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call updateAccessPermissions with the correct parameters when multiConvo is true', async () => {
|
||||
const config = { interface: { multiConvo: true } };
|
||||
const configDefaults = { interface: {} };
|
||||
|
||||
await loadDefaultInterface(config, configDefaults);
|
||||
|
||||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call updateAccessPermissions with false when multiConvo is false', async () => {
|
||||
const config = { interface: { multiConvo: false } };
|
||||
const configDefaults = { interface: {} };
|
||||
|
||||
await loadDefaultInterface(config, configDefaults);
|
||||
|
||||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call updateAccessPermissions with undefined when multiConvo is not specified in config', async () => {
|
||||
const config = {};
|
||||
const configDefaults = { interface: {} };
|
||||
|
||||
await loadDefaultInterface(config, configDefaults);
|
||||
|
||||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call updateAccessPermissions with all interface options including multiConvo', async () => {
|
||||
const config = { interface: { prompts: true, bookmarks: false, multiConvo: true } };
|
||||
const configDefaults = { interface: {} };
|
||||
|
||||
await loadDefaultInterface(config, configDefaults);
|
||||
|
||||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default values for multiConvo when config is undefined', async () => {
|
||||
const config = undefined;
|
||||
const configDefaults = { interface: { prompts: true, bookmarks: true, multiConvo: false } };
|
||||
|
||||
await loadDefaultInterface(config, configDefaults);
|
||||
|
||||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,60 @@
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports ServerRequest
|
||||
* @typedef {import('express').Request} ServerRequest
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports ServerResponse
|
||||
* @typedef {import('express').Response} ServerResponse
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports AgentRun
|
||||
* @typedef {import('@librechat/agents').Run} AgentRun
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports IState
|
||||
* @typedef {import('@librechat/agents').IState} IState
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports ClientCallbacks
|
||||
* @typedef {import('@librechat/agents').ClientCallbacks} ClientCallbacks
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports BedrockClientOptions
|
||||
* @typedef {import('@librechat/agents').BedrockConverseClientOptions} BedrockClientOptions
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports StreamEventData
|
||||
* @typedef {import('@librechat/agents').StreamEventData} StreamEventData
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports ToolEndData
|
||||
* @typedef {import('@librechat/agents').ToolEndData} ToolEndData
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports UsageMetadata
|
||||
* @typedef {import('@langchain/core/messages').UsageMetadata} UsageMetadata
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports Ollama
|
||||
* @typedef {import('ollama').Ollama} Ollama
|
||||
@@ -724,6 +778,36 @@
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports Agent
|
||||
* @typedef {import('librechat-data-provider').Agent} Agent
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports AgentCreateParams
|
||||
* @typedef {import('librechat-data-provider').AgentCreateParams} AgentCreateParams
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports AgentUpdateParams
|
||||
* @typedef {import('librechat-data-provider').AgentUpdateParams} AgentUpdateParams
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports AgentListParams
|
||||
* @typedef {import('librechat-data-provider').AgentListParams} AgentListParams
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports AgentListResponse
|
||||
* @typedef {import('librechat-data-provider').AgentListResponse} AgentListResponse
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents details of the message creation by the run step, including the ID of the created message.
|
||||
*
|
||||
@@ -833,6 +917,12 @@
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports TAgentClient
|
||||
* @typedef {import('./server/controllers/agents/client')} TAgentClient
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports ImportBatchBuilder
|
||||
* @typedef {import('./server/utils/import/importBatchBuilder.js').ImportBatchBuilder} ImportBatchBuilder
|
||||
|
||||
@@ -7,13 +7,13 @@ const openAIModels = {
|
||||
'gpt-4-32k': 32758, // -10 from max
|
||||
'gpt-4-32k-0314': 32758, // -10 from max
|
||||
'gpt-4-32k-0613': 32758, // -10 from max
|
||||
'gpt-4-1106': 127990, // -10 from max
|
||||
'gpt-4-0125': 127990, // -10 from max
|
||||
'gpt-4o': 127990, // -10 from max
|
||||
'gpt-4o-mini': 127990, // -10 from max
|
||||
'gpt-4o-2024-08-06': 127990, // -10 from max
|
||||
'gpt-4-turbo': 127990, // -10 from max
|
||||
'gpt-4-vision': 127990, // -10 from max
|
||||
'gpt-4-1106': 127500, // -500 from max
|
||||
'gpt-4-0125': 127500, // -500 from max
|
||||
'gpt-4o': 127500, // -500 from max
|
||||
'gpt-4o-mini': 127500, // -500 from max
|
||||
'gpt-4o-2024-08-06': 127500, // -500 from max
|
||||
'gpt-4-turbo': 127500, // -500 from max
|
||||
'gpt-4-vision': 127500, // -500 from max
|
||||
'gpt-3.5-turbo': 16375, // -10 from max
|
||||
'gpt-3.5-turbo-0613': 4092, // -5 from max
|
||||
'gpt-3.5-turbo-0301': 4092, // -5 from max
|
||||
@@ -21,9 +21,15 @@ const openAIModels = {
|
||||
'gpt-3.5-turbo-16k-0613': 16375, // -10 from max
|
||||
'gpt-3.5-turbo-1106': 16375, // -10 from max
|
||||
'gpt-3.5-turbo-0125': 16375, // -10 from max
|
||||
};
|
||||
|
||||
const mistralModels = {
|
||||
'mistral-': 31990, // -10 from max
|
||||
llama3: 8187, // -5 from max
|
||||
'llama-3': 8187, // -5 from max
|
||||
'mistral-7b': 31990, // -10 from max
|
||||
'mistral-small': 31990, // -10 from max
|
||||
'mixtral-8x7b': 31990, // -10 from max
|
||||
'mistral-large-2402': 127500,
|
||||
'mistral-large-2407': 127500,
|
||||
};
|
||||
|
||||
const cohereModels = {
|
||||
@@ -54,6 +60,7 @@ const googleModels = {
|
||||
|
||||
const anthropicModels = {
|
||||
'claude-': 100000,
|
||||
'claude-instant': 100000,
|
||||
'claude-2': 100000,
|
||||
'claude-2.1': 200000,
|
||||
'claude-3-haiku': 200000,
|
||||
@@ -63,14 +70,47 @@ const anthropicModels = {
|
||||
'claude-3.5-sonnet': 200000,
|
||||
};
|
||||
|
||||
const aggregateModels = { ...openAIModels, ...googleModels, ...anthropicModels, ...cohereModels };
|
||||
const metaModels = {
|
||||
'llama2-13b': 4000,
|
||||
'llama2-70b': 4000,
|
||||
'llama3-8b': 8000,
|
||||
'llama3-70b': 8000,
|
||||
'llama3-1-8b': 127500,
|
||||
'llama3-1-70b': 127500,
|
||||
'llama3-1-405b': 127500,
|
||||
};
|
||||
|
||||
const ai21Models = {
|
||||
'ai21.j2-mid-v1': 8182, // -10 from max
|
||||
'ai21.j2-ultra-v1': 8182, // -10 from max
|
||||
'ai21.jamba-instruct-v1:0': 255500, // -500 from max
|
||||
};
|
||||
|
||||
const amazonModels = {
|
||||
'amazon.titan-text-lite-v1': 4000,
|
||||
'amazon.titan-text-express-v1': 8000,
|
||||
'amazon.titan-text-premier-v1:0': 31500, // -500 from max
|
||||
};
|
||||
|
||||
const bedrockModels = {
|
||||
...anthropicModels,
|
||||
...mistralModels,
|
||||
...cohereModels,
|
||||
...metaModels,
|
||||
...ai21Models,
|
||||
...amazonModels,
|
||||
};
|
||||
|
||||
const aggregateModels = { ...openAIModels, ...googleModels, ...bedrockModels };
|
||||
|
||||
const maxTokensMap = {
|
||||
[EModelEndpoint.azureOpenAI]: openAIModels,
|
||||
[EModelEndpoint.openAI]: aggregateModels,
|
||||
[EModelEndpoint.agents]: aggregateModels,
|
||||
[EModelEndpoint.custom]: aggregateModels,
|
||||
[EModelEndpoint.google]: googleModels,
|
||||
[EModelEndpoint.anthropic]: anthropicModels,
|
||||
[EModelEndpoint.bedrock]: bedrockModels,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,18 +20,6 @@ describe('getModelMaxTokens', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should return correct tokens for LLama 3 models', () => {
|
||||
expect(getModelMaxTokens('meta-llama/llama-3-8b')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['llama-3'],
|
||||
);
|
||||
expect(getModelMaxTokens('meta-llama/llama-3-8b')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['llama3'],
|
||||
);
|
||||
expect(getModelMaxTokens('llama-3-500b')).toBe(maxTokensMap[EModelEndpoint.openAI]['llama-3']);
|
||||
expect(getModelMaxTokens('llama3-70b')).toBe(maxTokensMap[EModelEndpoint.openAI]['llama3']);
|
||||
expect(getModelMaxTokens('llama3:latest')).toBe(maxTokensMap[EModelEndpoint.openAI]['llama3']);
|
||||
});
|
||||
|
||||
test('should return undefined for no match', () => {
|
||||
expect(getModelMaxTokens('unknown-model')).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/assets/favicon-16x16.png"
|
||||
/>
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href="/assets/apple-touch-icon-180x180.png"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/frontend",
|
||||
"version": "v0.7.5-rc1",
|
||||
"version": "v0.7.5-rc2",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -65,6 +65,7 @@
|
||||
"filenamify": "^6.0.0",
|
||||
"html-to-image": "^1.11.11",
|
||||
"image-blob-reduce": "^4.1.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"librechat-data-provider": "*",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.394.0",
|
||||
@@ -111,6 +112,7 @@
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^20.3.0",
|
||||
"@types/react": "^18.2.11",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
@@ -130,11 +132,10 @@
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-loader": "^7.1.0",
|
||||
"postcss-preset-env": "^8.2.0",
|
||||
"rollup": "^4.21.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-jest": "^29.1.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^5.4.2",
|
||||
"vite": "^5.1.1",
|
||||
"vite-plugin-node-polyfills": "^0.17.0",
|
||||
"vite-plugin-pwa": "^0.19.8"
|
||||
}
|
||||
|
||||
27
client/src/Providers/AgentsContext.tsx
Normal file
27
client/src/Providers/AgentsContext.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { createContext, useContext } from 'react';
|
||||
import { defaultAgentFormValues } from 'librechat-data-provider';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
|
||||
type AgentsContextType = UseFormReturn<AgentForm>;
|
||||
|
||||
export const AgentsContext = createContext<AgentsContextType>({} as AgentsContextType);
|
||||
|
||||
export function useAgentsContext() {
|
||||
const context = useContext(AgentsContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useAgentsContext must be used within an AgentsProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
export default function AgentsProvider({ children }) {
|
||||
const methods = useForm<AgentForm>({
|
||||
defaultValues: defaultAgentFormValues,
|
||||
});
|
||||
|
||||
return <FormProvider {...methods}>{children}</FormProvider>;
|
||||
}
|
||||
6
client/src/Providers/AgentsMapContext.tsx
Normal file
6
client/src/Providers/AgentsMapContext.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import useAgentsMap from '~/hooks/Agents/useAgentsMap';
|
||||
type AgentsMapContextType = ReturnType<typeof useAgentsMap>;
|
||||
|
||||
export const AgentsMapContext = createContext<AgentsMapContextType>({} as AgentsMapContextType);
|
||||
export const useAgentsMapContext = () => useContext(AgentsMapContext);
|
||||
@@ -1,12 +1,6 @@
|
||||
// AnnouncerContext.tsx
|
||||
import React from 'react';
|
||||
|
||||
export interface AnnounceOptions {
|
||||
message: string;
|
||||
id?: string;
|
||||
isStream?: boolean;
|
||||
isComplete?: boolean;
|
||||
}
|
||||
import type { AnnounceOptions } from '~/common';
|
||||
|
||||
interface AnnouncerContextType {
|
||||
announceAssertive: (options: AnnounceOptions) => void;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user