mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-25 11:46:12 +01:00
Merge branch 'main' into refactor/openai-moderation
This commit is contained in:
commit
14c974d07f
269 changed files with 6809 additions and 2571 deletions
|
|
@ -119,6 +119,7 @@ BINGAI_TOKEN=user_provided
|
|||
# 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
|
||||
# BEDROCK_AWS_SESSION_TOKEN=someSessionToken
|
||||
|
||||
# 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
|
||||
|
|
@ -140,7 +141,7 @@ GOOGLE_KEY=user_provided
|
|||
# GOOGLE_REVERSE_PROXY=
|
||||
|
||||
# Gemini API (AI Studio)
|
||||
# GOOGLE_MODELS=gemini-exp-1121,gemini-exp-1114,gemini-1.5-flash-latest,gemini-1.0-pro,gemini-1.0-pro-001,gemini-1.0-pro-latest,gemini-1.0-pro-vision-latest,gemini-1.5-pro-latest,gemini-pro,gemini-pro-vision
|
||||
# GOOGLE_MODELS=gemini-2.0-flash-exp,gemini-exp-1121,gemini-exp-1114,gemini-1.5-flash-latest,gemini-1.0-pro,gemini-1.0-pro-001,gemini-1.0-pro-latest,gemini-1.0-pro-vision-latest,gemini-1.5-pro-latest,gemini-pro,gemini-pro-vision
|
||||
|
||||
# Vertex AI
|
||||
# GOOGLE_MODELS=gemini-1.5-flash-preview-0514,gemini-1.5-pro-preview-0514,gemini-1.0-pro-vision-001,gemini-1.0-pro-002,gemini-1.0-pro-001,gemini-pro-vision,gemini-1.0-pro
|
||||
|
|
@ -177,10 +178,10 @@ OPENAI_API_KEY=user_provided
|
|||
DEBUG_OPENAI=false
|
||||
|
||||
# TITLE_CONVO=false
|
||||
# OPENAI_TITLE_MODEL=gpt-3.5-turbo
|
||||
# OPENAI_TITLE_MODEL=gpt-4o-mini
|
||||
|
||||
# OPENAI_SUMMARIZE=true
|
||||
# OPENAI_SUMMARY_MODEL=gpt-3.5-turbo
|
||||
# OPENAI_SUMMARY_MODEL=gpt-4o-mini
|
||||
|
||||
# OPENAI_FORCE_PROMPT=true
|
||||
|
||||
|
|
|
|||
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
|
|
@ -10,7 +10,8 @@
|
|||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
},
|
||||
"console": "integratedTerminal"
|
||||
"console": "integratedTerminal",
|
||||
"envFile": "${workspaceFolder}/.env"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
97
README.md
97
README.md
|
|
@ -38,42 +38,75 @@
|
|||
</a>
|
||||
</p>
|
||||
|
||||
# 📃 Features
|
||||
# ✨ Features
|
||||
|
||||
- 🖥️ UI matching ChatGPT, including Dark mode, Streaming, and latest updates
|
||||
- 🤖 AI model selection:
|
||||
- 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
|
||||
- 🌿 Fork Messages & Conversations for Advanced Context control
|
||||
- 💬 Multimodal Chat:
|
||||
- Upload and analyze images with Claude 3, GPT-4 (including `gpt-4o` and `gpt-4o-mini`), and Gemini Vision 📸
|
||||
- Chat with Files using Custom Endpoints, OpenAI, Azure, Anthropic, & Google. 🗃️
|
||||
- Advanced Agents with Files, Code Interpreter, Tools, and API Actions 🔦
|
||||
- Available through the [OpenAI Assistants API](https://platform.openai.com/docs/assistants/overview) 🌤️
|
||||
- Non-OpenAI Agents in Active Development 🚧
|
||||
- 🌎 Multilingual UI:
|
||||
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro,
|
||||
- 🖥️ **UI & Experience** inspired by ChatGPT with enhanced design and features
|
||||
|
||||
- 🤖 **AI Model Selection**:
|
||||
- Anthropic (Claude), AWS Bedrock, OpenAI, Azure OpenAI, Google, Vertex AI, OpenAI Assistants API (incl. Azure)
|
||||
- [Custom Endpoints](https://www.librechat.ai/docs/quick_start/custom_endpoints): Use any OpenAI-compatible API with LibreChat, no proxy required
|
||||
- Compatible with [Local & Remote AI Providers](https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints):
|
||||
- Ollama, groq, Cohere, Mistral AI, Apple MLX, koboldcpp, together.ai,
|
||||
- OpenRouter, Perplexity, ShuttleAI, Deepseek, Qwen, and more
|
||||
|
||||
- 🔧 **[Code Interpreter API](https://www.librechat.ai/docs/features/code_interpreter)**:
|
||||
- Secure, Sandboxed Execution in Python, Node.js (JS/TS), Go, C/C++, Java, PHP, Rust, and Fortran
|
||||
- Seamless File Handling: Upload, process, and download files directly
|
||||
- No Privacy Concerns: Fully isolated and secure execution
|
||||
|
||||
- 🔦 **Agents & Tools Integration**:
|
||||
- **[LibreChat Agents](https://www.librechat.ai/docs/features/agents)**:
|
||||
- No-Code Custom Assistants: Build specialized, AI-driven helpers without coding
|
||||
- Flexible & Extensible: Attach tools like DALL-E-3, file search, code execution, and more
|
||||
- Compatible with Custom Endpoints, OpenAI, Azure, Anthropic, AWS Bedrock, and more
|
||||
- Use LibreChat Agents and OpenAI Assistants with Files, Code Interpreter, Tools, and API Actions
|
||||
|
||||
- 🪄 **Generative UI with Code Artifacts**:
|
||||
- [Code Artifacts](https://youtu.be/GfTj7O4gmd0?si=WJbdnemZpJzBrJo3) allow creation of React, HTML, and Mermaid diagrams directly in chat
|
||||
|
||||
- 💾 **Presets & Context Management**:
|
||||
- Create, Save, & Share Custom Presets
|
||||
- Switch between AI Endpoints and Presets mid-chat
|
||||
- Edit, Resubmit, and Continue Messages with Conversation branching
|
||||
- [Fork Messages & Conversations](https://www.librechat.ai/docs/features/fork) for Advanced Context control
|
||||
|
||||
- 💬 **Multimodal & File Interactions**:
|
||||
- Upload and analyze images with Claude 3, GPT-4o, o1, Llama-Vision, and Gemini 📸
|
||||
- Chat with Files using Custom Endpoints, OpenAI, Azure, Anthropic, AWS Bedrock, & Google 🗃️
|
||||
|
||||
- 🌎 **Multilingual UI**:
|
||||
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro
|
||||
- Русский, 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands, עברית
|
||||
- 🎨 Customizable Dropdown & Interface: Adapts to both power users and newcomers
|
||||
- 📧 Verify your email to ensure secure access
|
||||
- 🗣️ Chat hands-free with Speech-to-Text and Text-to-Speech magic
|
||||
- Automatically send and play Audio
|
||||
|
||||
- 🎨 **Customizable Interface**:
|
||||
- Customizable Dropdown & Interface that adapts to both power users and newcomers
|
||||
|
||||
- 📧 **Secure Access**:
|
||||
- Verify your email to ensure secure access
|
||||
|
||||
- 🗣️ **Speech & Audio**:
|
||||
- Chat hands-free with Speech-to-Text and Text-to-Speech
|
||||
- Automatically send and play Audio
|
||||
- Supports OpenAI, Azure OpenAI, and Elevenlabs
|
||||
- 📥 Import Conversations from LibreChat, ChatGPT, Chatbot UI
|
||||
- 📤 Export conversations as screenshots, markdown, text, json
|
||||
- 🔍 Search all messages/conversations
|
||||
- 🔌 Plugins, including web access, image generation with DALL-E-3 and more
|
||||
- 👥 Multi-User, Secure Authentication with Moderation and Token spend tools
|
||||
- ⚙️ Configure Proxy, Reverse Proxy, Docker, & many Deployment options:
|
||||
|
||||
- 📥 **Import & Export Conversations**:
|
||||
- Import Conversations from LibreChat, ChatGPT, Chatbot UI
|
||||
- Export conversations as screenshots, markdown, text, json
|
||||
|
||||
- 🔍 **Search & Discovery**:
|
||||
- Search all messages/conversations
|
||||
|
||||
- 👥 **Multi-User & Secure**:
|
||||
- Multi-User, Secure Authentication with OAuth2 & Email Login Support
|
||||
- Built-in Moderation, and Token spend tools
|
||||
|
||||
- ⚙️ **Configuration & Deployment**:
|
||||
- Configure Proxy, Reverse Proxy, Docker, & many Deployment options
|
||||
- Use completely local or deploy on the cloud
|
||||
- 📖 Completely Open-Source & Built in Public
|
||||
- 🧑🤝🧑 Community-driven development, support, and feedback
|
||||
|
||||
- 📖 **Open-Source & Community**:
|
||||
- Completely Open-Source & Built in Public
|
||||
- Community-driven development, support, and feedback
|
||||
|
||||
[For a thorough review of our features, see our docs here](https://docs.librechat.ai/) 📚
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ class BaseClient {
|
|||
/** The key for the usage object's output tokens
|
||||
* @type {string} */
|
||||
this.outputTokensKey = 'completion_tokens';
|
||||
/** @type {Set<string>} */
|
||||
this.savedMessageIds = new Set();
|
||||
}
|
||||
|
||||
setOptions() {
|
||||
|
|
@ -84,7 +86,7 @@ class BaseClient {
|
|||
return this.options.agent.id;
|
||||
}
|
||||
|
||||
return this.modelOptions.model;
|
||||
return this.modelOptions?.model ?? this.model;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -508,7 +510,7 @@ class BaseClient {
|
|||
conversationId,
|
||||
parentMessageId: userMessage.messageId,
|
||||
isCreatedByUser: false,
|
||||
model: this.modelOptions.model,
|
||||
model: this.modelOptions?.model ?? this.model,
|
||||
sender: this.sender,
|
||||
text: generation,
|
||||
};
|
||||
|
|
@ -545,6 +547,7 @@ class BaseClient {
|
|||
|
||||
if (!isEdited && !this.skipSaveUserMessage) {
|
||||
this.userMessagePromise = this.saveMessageToDatabase(userMessage, saveOptions, user);
|
||||
this.savedMessageIds.add(userMessage.messageId);
|
||||
if (typeof opts?.getReqData === 'function') {
|
||||
opts.getReqData({
|
||||
userMessagePromise: this.userMessagePromise,
|
||||
|
|
@ -563,8 +566,8 @@ class BaseClient {
|
|||
user: this.user,
|
||||
tokenType: 'prompt',
|
||||
amount: promptTokens,
|
||||
model: this.modelOptions.model,
|
||||
endpoint: this.options.endpoint,
|
||||
model: this.modelOptions?.model ?? this.model,
|
||||
endpointTokenConfig: this.options.endpointTokenConfig,
|
||||
},
|
||||
});
|
||||
|
|
@ -574,6 +577,7 @@ class BaseClient {
|
|||
const completion = await this.sendCompletion(payload, opts);
|
||||
this.abortController.requestCompleted = true;
|
||||
|
||||
/** @type {TMessage} */
|
||||
const responseMessage = {
|
||||
messageId: responseMessageId,
|
||||
conversationId,
|
||||
|
|
@ -635,7 +639,16 @@ class BaseClient {
|
|||
responseMessage.attachments = (await Promise.all(this.artifactPromises)).filter((a) => a);
|
||||
}
|
||||
|
||||
if (this.options.attachments) {
|
||||
try {
|
||||
saveOptions.files = this.options.attachments.map((attachments) => attachments.file_id);
|
||||
} catch (error) {
|
||||
logger.error('[BaseClient] Error mapping attachments for conversation', error);
|
||||
}
|
||||
}
|
||||
|
||||
this.responsePromise = this.saveMessageToDatabase(responseMessage, saveOptions, user);
|
||||
this.savedMessageIds.add(responseMessage.messageId);
|
||||
const messageCache = getLogStores(CacheKeys.MESSAGES);
|
||||
messageCache.set(
|
||||
responseMessageId,
|
||||
|
|
@ -902,8 +915,9 @@ class BaseClient {
|
|||
// Note: gpt-3.5-turbo and gpt-4 may update over time. Use default for these as well as for unknown models
|
||||
let tokensPerMessage = 3;
|
||||
let tokensPerName = 1;
|
||||
const model = this.modelOptions?.model ?? this.model;
|
||||
|
||||
if (this.modelOptions.model === 'gpt-3.5-turbo-0301') {
|
||||
if (model === 'gpt-3.5-turbo-0301') {
|
||||
tokensPerMessage = 4;
|
||||
tokensPerName = -1;
|
||||
}
|
||||
|
|
@ -961,6 +975,15 @@ class BaseClient {
|
|||
return _messages;
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
const attachmentsProcessed =
|
||||
this.options.attachments && !(this.options.attachments instanceof Promise);
|
||||
if (attachmentsProcessed) {
|
||||
for (const attachment of this.options.attachments) {
|
||||
seen.add(attachment.file_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TMessage} message
|
||||
|
|
@ -971,7 +994,19 @@ class BaseClient {
|
|||
this.message_file_map = {};
|
||||
}
|
||||
|
||||
const fileIds = message.files.map((file) => file.file_id);
|
||||
const fileIds = [];
|
||||
for (const file of message.files) {
|
||||
if (seen.has(file.file_id)) {
|
||||
continue;
|
||||
}
|
||||
fileIds.push(file.file_id);
|
||||
seen.add(file.file_id);
|
||||
}
|
||||
|
||||
if (fileIds.length === 0) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const files = await getFiles({
|
||||
file_id: { $in: fileIds },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -227,6 +227,16 @@ class ChatGPTClient extends BaseClient {
|
|||
this.azure = !serverless && azureOptions;
|
||||
this.azureEndpoint =
|
||||
!serverless && genAzureChatCompletion(this.azure, modelOptions.model, this);
|
||||
if (serverless === true) {
|
||||
this.options.defaultQuery = azureOptions.azureOpenAIApiVersion
|
||||
? { 'api-version': azureOptions.azureOpenAIApiVersion }
|
||||
: undefined;
|
||||
this.options.headers['api-key'] = this.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.defaultQuery) {
|
||||
opts.defaultQuery = this.options.defaultQuery;
|
||||
}
|
||||
|
||||
if (this.options.headers) {
|
||||
|
|
|
|||
|
|
@ -688,7 +688,7 @@ class OpenAIClient extends BaseClient {
|
|||
}
|
||||
|
||||
initializeLLM({
|
||||
model = 'gpt-3.5-turbo',
|
||||
model = 'gpt-4o-mini',
|
||||
modelName,
|
||||
temperature = 0.2,
|
||||
presence_penalty = 0,
|
||||
|
|
@ -793,7 +793,7 @@ class OpenAIClient extends BaseClient {
|
|||
|
||||
const { OPENAI_TITLE_MODEL } = process.env ?? {};
|
||||
|
||||
let model = this.options.titleModel ?? OPENAI_TITLE_MODEL ?? 'gpt-3.5-turbo';
|
||||
let model = this.options.titleModel ?? OPENAI_TITLE_MODEL ?? 'gpt-4o-mini';
|
||||
if (model === Constants.CURRENT_MODEL) {
|
||||
model = this.modelOptions.model;
|
||||
}
|
||||
|
|
@ -838,6 +838,12 @@ class OpenAIClient extends BaseClient {
|
|||
this.options.dropParams = azureConfig.groupMap[groupName].dropParams;
|
||||
this.options.forcePrompt = azureConfig.groupMap[groupName].forcePrompt;
|
||||
this.azure = !serverless && azureOptions;
|
||||
if (serverless === true) {
|
||||
this.options.defaultQuery = azureOptions.azureOpenAIApiVersion
|
||||
? { 'api-version': azureOptions.azureOpenAIApiVersion }
|
||||
: undefined;
|
||||
this.options.headers['api-key'] = this.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
const titleChatCompletion = async () => {
|
||||
|
|
@ -976,7 +982,7 @@ ${convo}
|
|||
let prompt;
|
||||
|
||||
// TODO: remove the gpt fallback and make it specific to endpoint
|
||||
const { OPENAI_SUMMARY_MODEL = 'gpt-3.5-turbo' } = process.env ?? {};
|
||||
const { OPENAI_SUMMARY_MODEL = 'gpt-4o-mini' } = process.env ?? {};
|
||||
let model = this.options.summaryModel ?? OPENAI_SUMMARY_MODEL;
|
||||
if (model === Constants.CURRENT_MODEL) {
|
||||
model = this.modelOptions.model;
|
||||
|
|
@ -1169,6 +1175,10 @@ ${convo}
|
|||
opts.defaultHeaders = { ...opts.defaultHeaders, ...this.options.headers };
|
||||
}
|
||||
|
||||
if (this.options.defaultQuery) {
|
||||
opts.defaultQuery = this.options.defaultQuery;
|
||||
}
|
||||
|
||||
if (this.options.proxy) {
|
||||
opts.httpAgent = new HttpsProxyAgent(this.options.proxy);
|
||||
}
|
||||
|
|
@ -1207,6 +1217,12 @@ ${convo}
|
|||
this.azure = !serverless && azureOptions;
|
||||
this.azureEndpoint =
|
||||
!serverless && genAzureChatCompletion(this.azure, modelOptions.model, this);
|
||||
if (serverless === true) {
|
||||
this.options.defaultQuery = azureOptions.azureOpenAIApiVersion
|
||||
? { 'api-version': azureOptions.azureOpenAIApiVersion }
|
||||
: undefined;
|
||||
this.options.headers['api-key'] = this.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.azure || this.options.azure) {
|
||||
|
|
@ -1308,6 +1324,11 @@ ${convo}
|
|||
/** @type {(value: void | PromiseLike<void>) => void} */
|
||||
let streamResolve;
|
||||
|
||||
if (this.isO1Model === true && this.azure && modelOptions.stream) {
|
||||
delete modelOptions.stream;
|
||||
delete modelOptions.stop;
|
||||
}
|
||||
|
||||
if (modelOptions.stream) {
|
||||
streamPromise = new Promise((resolve) => {
|
||||
streamResolve = resolve;
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class PluginsClient extends OpenAIClient {
|
|||
chatHistory: new ChatMessageHistory(pastMessages),
|
||||
});
|
||||
|
||||
this.tools = await loadTools({
|
||||
const { loadedTools } = await loadTools({
|
||||
user,
|
||||
model,
|
||||
tools: this.options.tools,
|
||||
|
|
@ -119,12 +119,15 @@ class PluginsClient extends OpenAIClient {
|
|||
processFileURL,
|
||||
message,
|
||||
},
|
||||
useSpecs: true,
|
||||
});
|
||||
|
||||
if (this.tools.length === 0) {
|
||||
if (loadedTools.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tools = loadedTools;
|
||||
|
||||
logger.debug('[PluginsClient] Requested Tools', this.options.tools);
|
||||
logger.debug(
|
||||
'[PluginsClient] Loaded Tools',
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const { isEnabled } = require('~/server/utils');
|
|||
*
|
||||
* @example
|
||||
* const llm = createLLM({
|
||||
* modelOptions: { modelName: 'gpt-3.5-turbo', temperature: 0.2 },
|
||||
* modelOptions: { modelName: 'gpt-4o-mini', temperature: 0.2 },
|
||||
* configOptions: { basePath: 'https://example.api/path' },
|
||||
* callbacks: { onMessage: handleMessage },
|
||||
* openAIApiKey: 'your-api-key'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const { ChatOpenAI } = require('@langchain/openai');
|
|||
const { getBufferString, ConversationSummaryBufferMemory } = require('langchain/memory');
|
||||
|
||||
const chatPromptMemory = new ConversationSummaryBufferMemory({
|
||||
llm: new ChatOpenAI({ modelName: 'gpt-3.5-turbo', temperature: 0 }),
|
||||
llm: new ChatOpenAI({ modelName: 'gpt-4o-mini', temperature: 0 }),
|
||||
maxTokenLimit: 10,
|
||||
returnMessages: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ const formatAgentMessages = (payload) => {
|
|||
new ToolMessage({
|
||||
tool_call_id: tool_call.id,
|
||||
name: tool_call.name,
|
||||
content: output,
|
||||
content: output || '',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ describe('BaseClient', () => {
|
|||
const options = {
|
||||
// debug: true,
|
||||
modelOptions: {
|
||||
model: 'gpt-3.5-turbo',
|
||||
model: 'gpt-4o-mini',
|
||||
temperature: 0,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ describe('OpenAIClient', () => {
|
|||
|
||||
it('should set isChatCompletion based on useOpenRouter, reverseProxyUrl, or model', () => {
|
||||
client.setOptions({ reverseProxyUrl: null });
|
||||
// true by default since default model will be gpt-3.5-turbo
|
||||
// true by default since default model will be gpt-4o-mini
|
||||
expect(client.isChatCompletion).toBe(true);
|
||||
client.isChatCompletion = undefined;
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ describe('OpenAIClient', () => {
|
|||
expect(client.isChatCompletion).toBe(false);
|
||||
client.isChatCompletion = undefined;
|
||||
|
||||
client.setOptions({ modelOptions: { model: 'gpt-3.5-turbo' }, reverseProxyUrl: null });
|
||||
client.setOptions({ modelOptions: { model: 'gpt-4o-mini' }, reverseProxyUrl: null });
|
||||
expect(client.isChatCompletion).toBe(true);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ class DALLE3 extends Tool {
|
|||
|
||||
this.userId = fields.userId;
|
||||
this.fileStrategy = fields.fileStrategy;
|
||||
/** @type {boolean} */
|
||||
this.isAgent = fields.isAgent;
|
||||
if (fields.processFileURL) {
|
||||
/** @type {processFileURL} Necessary for output to contain all image metadata. */
|
||||
this.processFileURL = fields.processFileURL.bind(this);
|
||||
|
|
@ -108,6 +110,19 @@ class DALLE3 extends Tool {
|
|||
return ``;
|
||||
}
|
||||
|
||||
returnValue(value) {
|
||||
if (this.isAgent === true && typeof value === 'string') {
|
||||
return [value, {}];
|
||||
} else if (this.isAgent === true && typeof value === 'object') {
|
||||
return [
|
||||
'DALL-E displayed an image. All generated images are already plainly visible, so don\'t repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user.',
|
||||
value,
|
||||
];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async _call(data) {
|
||||
const { prompt, quality = 'standard', size = '1024x1024', style = 'vivid' } = data;
|
||||
if (!prompt) {
|
||||
|
|
@ -126,18 +141,23 @@ class DALLE3 extends Tool {
|
|||
});
|
||||
} catch (error) {
|
||||
logger.error('[DALL-E-3] Problem generating the image:', error);
|
||||
return `Something went wrong when trying to generate the image. The DALL-E API may be unavailable:
|
||||
Error Message: ${error.message}`;
|
||||
return this
|
||||
.returnValue(`Something went wrong when trying to generate the image. The DALL-E API may be unavailable:
|
||||
Error Message: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!resp) {
|
||||
return 'Something went wrong when trying to generate the image. The DALL-E API may be unavailable';
|
||||
return this.returnValue(
|
||||
'Something went wrong when trying to generate the image. The DALL-E API may be unavailable',
|
||||
);
|
||||
}
|
||||
|
||||
const theImageUrl = resp.data[0].url;
|
||||
|
||||
if (!theImageUrl) {
|
||||
return 'No image URL returned from OpenAI API. There may be a problem with the API or your configuration.';
|
||||
return this.returnValue(
|
||||
'No image URL returned from OpenAI API. There may be a problem with the API or your configuration.',
|
||||
);
|
||||
}
|
||||
|
||||
const imageBasename = getImageBasename(theImageUrl);
|
||||
|
|
@ -157,11 +177,11 @@ Error Message: ${error.message}`;
|
|||
|
||||
try {
|
||||
const result = await this.processFileURL({
|
||||
fileStrategy: this.fileStrategy,
|
||||
userId: this.userId,
|
||||
URL: theImageUrl,
|
||||
fileName: imageName,
|
||||
basePath: 'images',
|
||||
userId: this.userId,
|
||||
fileName: imageName,
|
||||
fileStrategy: this.fileStrategy,
|
||||
context: FileContext.image_generation,
|
||||
});
|
||||
|
||||
|
|
@ -175,7 +195,7 @@ Error Message: ${error.message}`;
|
|||
this.result = `Failed to save the image locally. ${error.message}`;
|
||||
}
|
||||
|
||||
return this.result;
|
||||
return this.returnValue(this.result);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,20 +10,50 @@ const { logger } = require('~/config');
|
|||
* @param {Object} options
|
||||
* @param {ServerRequest} options.req
|
||||
* @param {Agent['tool_resources']} options.tool_resources
|
||||
* @returns {Promise<{
|
||||
* files: Array<{ file_id: string; filename: string }>,
|
||||
* toolContext: string
|
||||
* }>}
|
||||
*/
|
||||
const primeFiles = async (options) => {
|
||||
const { tool_resources } = options;
|
||||
const file_ids = tool_resources?.[EToolResources.file_search]?.file_ids ?? [];
|
||||
const agentResourceIds = new Set(file_ids);
|
||||
const resourceFiles = tool_resources?.[EToolResources.file_search]?.files ?? [];
|
||||
const dbFiles = ((await getFiles({ file_id: { $in: file_ids } })) ?? []).concat(resourceFiles);
|
||||
|
||||
let toolContext = `- Note: Semantic search is available through the ${Tools.file_search} tool but no files are currently loaded. Request the user to upload documents to search through.`;
|
||||
|
||||
const files = [];
|
||||
for (let i = 0; i < dbFiles.length; i++) {
|
||||
const file = dbFiles[i];
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
if (i === 0) {
|
||||
toolContext = `- Note: Use the ${Tools.file_search} tool to find relevant information within:`;
|
||||
}
|
||||
toolContext += `\n\t- ${file.filename}${
|
||||
agentResourceIds.has(file.file_id) ? '' : ' (just attached by user)'
|
||||
}`;
|
||||
files.push({
|
||||
file_id: file.file_id,
|
||||
filename: file.filename,
|
||||
});
|
||||
}
|
||||
|
||||
return { files, toolContext };
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {ServerRequest} options.req
|
||||
* @param {Array<{ file_id: string; filename: string }>} options.files
|
||||
* @returns
|
||||
*/
|
||||
const createFileSearchTool = async (options) => {
|
||||
const { req, tool_resources } = options;
|
||||
const file_ids = tool_resources?.[EToolResources.file_search]?.file_ids ?? [];
|
||||
const files = (await getFiles({ file_id: { $in: file_ids } })).map((file) => ({
|
||||
file_id: file.file_id,
|
||||
filename: file.filename,
|
||||
}));
|
||||
|
||||
const fileList = files.map((file) => `- ${file.filename}`).join('\n');
|
||||
const toolDescription = `Performs a semantic search based on a natural language query across the following files:\n${fileList}`;
|
||||
|
||||
const FileSearch = tool(
|
||||
const createFileSearchTool = async ({ req, files }) => {
|
||||
return tool(
|
||||
async ({ query }) => {
|
||||
if (files.length === 0) {
|
||||
return 'No files to search. Instruct the user to add files for the search.';
|
||||
|
|
@ -87,7 +117,7 @@ const createFileSearchTool = async (options) => {
|
|||
},
|
||||
{
|
||||
name: Tools.file_search,
|
||||
description: toolDescription,
|
||||
description: `Performs semantic search across attached "${Tools.file_search}" documents using natural language queries. This tool analyzes the content of uploaded files to find relevant information, quotes, and passages that best match your query. Use this to extract specific information or find relevant sections within the available documents.`,
|
||||
schema: z.object({
|
||||
query: z
|
||||
.string()
|
||||
|
|
@ -97,8 +127,6 @@ const createFileSearchTool = async (options) => {
|
|||
}),
|
||||
},
|
||||
);
|
||||
|
||||
return FileSearch;
|
||||
};
|
||||
|
||||
module.exports = createFileSearchTool;
|
||||
module.exports = { createFileSearchTool, primeFiles };
|
||||
|
|
@ -15,8 +15,8 @@ const {
|
|||
StructuredWolfram,
|
||||
TavilySearchResults,
|
||||
} = require('../');
|
||||
const { primeFiles } = require('~/server/services/Files/Code/process');
|
||||
const createFileSearchTool = require('./createFileSearchTool');
|
||||
const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process');
|
||||
const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch');
|
||||
const { loadSpecs } = require('./loadSpecs');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ const validateTools = async (user, tools = []) => {
|
|||
}
|
||||
};
|
||||
|
||||
const loadAuthValues = async ({ userId, authFields }) => {
|
||||
const loadAuthValues = async ({ userId, authFields, throwError = true }) => {
|
||||
let authValues = {};
|
||||
|
||||
/**
|
||||
|
|
@ -98,7 +98,7 @@ const loadAuthValues = async ({ userId, authFields }) => {
|
|||
return { authField: field, authValue: value };
|
||||
}
|
||||
try {
|
||||
value = await getUserPluginAuthValue(userId, field);
|
||||
value = await getUserPluginAuthValue(userId, field, throwError);
|
||||
} catch (err) {
|
||||
if (field === fields[fields.length - 1] && !value) {
|
||||
throw err;
|
||||
|
|
@ -122,15 +122,18 @@ const loadAuthValues = async ({ userId, authFields }) => {
|
|||
return authValues;
|
||||
};
|
||||
|
||||
/** @typedef {typeof import('@langchain/core/tools').Tool} ToolConstructor */
|
||||
/** @typedef {import('@langchain/core/tools').Tool} Tool */
|
||||
|
||||
/**
|
||||
* Initializes a tool with authentication values for the given user, supporting alternate authentication fields.
|
||||
* Authentication fields can have alternates separated by "||", and the first defined variable will be used.
|
||||
*
|
||||
* @param {string} userId The user ID for which the tool is being loaded.
|
||||
* @param {Array<string>} authFields Array of strings representing the authentication fields. Supports alternate fields delimited by "||".
|
||||
* @param {typeof import('langchain/tools').Tool} ToolConstructor The constructor function for the tool to be initialized.
|
||||
* @param {ToolConstructor} ToolConstructor The constructor function for the tool to be initialized.
|
||||
* @param {Object} options Optional parameters to be passed to the tool constructor alongside authentication values.
|
||||
* @returns {Function} An Async function that, when called, asynchronously initializes and returns an instance of the tool with authentication.
|
||||
* @returns {() => Promise<Tool>} An Async function that, when called, asynchronously initializes and returns an instance of the tool with authentication.
|
||||
*/
|
||||
const loadToolWithAuth = (userId, authFields, ToolConstructor, options = {}) => {
|
||||
return async function () {
|
||||
|
|
@ -142,11 +145,12 @@ const loadToolWithAuth = (userId, authFields, ToolConstructor, options = {}) =>
|
|||
const loadTools = async ({
|
||||
user,
|
||||
model,
|
||||
functions = true,
|
||||
returnMap = false,
|
||||
isAgent,
|
||||
useSpecs,
|
||||
tools = [],
|
||||
options = {},
|
||||
skipSpecs = false,
|
||||
functions = true,
|
||||
returnMap = false,
|
||||
}) => {
|
||||
const toolConstructors = {
|
||||
calculator: Calculator,
|
||||
|
|
@ -174,11 +178,12 @@ const loadTools = async ({
|
|||
|
||||
const requestedTools = {};
|
||||
|
||||
if (functions) {
|
||||
if (functions === true) {
|
||||
toolConstructors.dalle = DALLE3;
|
||||
}
|
||||
|
||||
const imageGenOptions = {
|
||||
isAgent,
|
||||
req: options.req,
|
||||
fileStrategy: options.fileStrategy,
|
||||
processFileURL: options.processFileURL,
|
||||
|
|
@ -189,7 +194,6 @@ const loadTools = async ({
|
|||
const toolOptions = {
|
||||
serpapi: { location: 'Austin,Texas,United States', hl: 'en', gl: 'us' },
|
||||
dalle: imageGenOptions,
|
||||
'dall-e': imageGenOptions,
|
||||
'stable-diffusion': imageGenOptions,
|
||||
};
|
||||
|
||||
|
|
@ -203,24 +207,38 @@ const loadTools = async ({
|
|||
toolAuthFields[tool.pluginKey] = tool.authConfig.map((auth) => auth.authField);
|
||||
});
|
||||
|
||||
const toolContextMap = {};
|
||||
const remainingTools = [];
|
||||
|
||||
for (const tool of tools) {
|
||||
if (tool === Tools.execute_code) {
|
||||
const authValues = await loadAuthValues({
|
||||
userId: user,
|
||||
authFields: [EnvVar.CODE_API_KEY],
|
||||
});
|
||||
const files = await primeFiles(options, authValues[EnvVar.CODE_API_KEY]);
|
||||
requestedTools[tool] = () =>
|
||||
createCodeExecutionTool({
|
||||
requestedTools[tool] = async () => {
|
||||
const authValues = await loadAuthValues({
|
||||
userId: user,
|
||||
authFields: [EnvVar.CODE_API_KEY],
|
||||
});
|
||||
const codeApiKey = authValues[EnvVar.CODE_API_KEY];
|
||||
const { files, toolContext } = await primeCodeFiles(options, codeApiKey);
|
||||
if (toolContext) {
|
||||
toolContextMap[tool] = toolContext;
|
||||
}
|
||||
const CodeExecutionTool = createCodeExecutionTool({
|
||||
user_id: user,
|
||||
files,
|
||||
...authValues,
|
||||
});
|
||||
CodeExecutionTool.apiKey = codeApiKey;
|
||||
return CodeExecutionTool;
|
||||
};
|
||||
continue;
|
||||
} else if (tool === Tools.file_search) {
|
||||
requestedTools[tool] = () => createFileSearchTool(options);
|
||||
requestedTools[tool] = async () => {
|
||||
const { files, toolContext } = await primeSearchFiles(options);
|
||||
if (toolContext) {
|
||||
toolContextMap[tool] = toolContext;
|
||||
}
|
||||
return createFileSearchTool({ req: options.req, files });
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -241,13 +259,13 @@ const loadTools = async ({
|
|||
continue;
|
||||
}
|
||||
|
||||
if (functions) {
|
||||
if (functions === true) {
|
||||
remainingTools.push(tool);
|
||||
}
|
||||
}
|
||||
|
||||
let specs = null;
|
||||
if (functions && remainingTools.length > 0 && skipSpecs !== true) {
|
||||
if (useSpecs === true && functions === true && remainingTools.length > 0) {
|
||||
specs = await loadSpecs({
|
||||
llm: model,
|
||||
user,
|
||||
|
|
@ -270,23 +288,21 @@ const loadTools = async ({
|
|||
return requestedTools;
|
||||
}
|
||||
|
||||
// load tools
|
||||
let result = [];
|
||||
const toolPromises = [];
|
||||
for (const tool of tools) {
|
||||
const validTool = requestedTools[tool];
|
||||
if (!validTool) {
|
||||
continue;
|
||||
}
|
||||
const plugin = await validTool();
|
||||
|
||||
if (Array.isArray(plugin)) {
|
||||
result = [...result, ...plugin];
|
||||
} else if (plugin) {
|
||||
result.push(plugin);
|
||||
if (validTool) {
|
||||
toolPromises.push(
|
||||
validTool().catch((error) => {
|
||||
logger.error(`Error loading tool ${tool}:`, error);
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
const loadedTools = (await Promise.all(toolPromises)).flatMap((plugin) => plugin || []);
|
||||
return { loadedTools, toolContextMap };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -128,12 +128,14 @@ describe('Tool Handlers', () => {
|
|||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
toolFunctions = await loadTools({
|
||||
const toolMap = await loadTools({
|
||||
user: fakeUser._id,
|
||||
model: BaseLLM,
|
||||
tools: sampleTools,
|
||||
returnMap: true,
|
||||
useSpecs: true,
|
||||
});
|
||||
toolFunctions = toolMap;
|
||||
loadTool1 = toolFunctions[sampleTools[0]];
|
||||
loadTool2 = toolFunctions[sampleTools[1]];
|
||||
loadTool3 = toolFunctions[sampleTools[2]];
|
||||
|
|
@ -195,6 +197,7 @@ describe('Tool Handlers', () => {
|
|||
expect(mockPluginService.getUserPluginAuthValue).toHaveBeenCalledWith(
|
||||
'userId',
|
||||
'DALLE3_API_KEY',
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -224,6 +227,7 @@ describe('Tool Handlers', () => {
|
|||
user: fakeUser._id,
|
||||
model: BaseLLM,
|
||||
returnMap: true,
|
||||
useSpecs: true,
|
||||
});
|
||||
expect(toolFunctions).toEqual({});
|
||||
});
|
||||
|
|
@ -235,6 +239,7 @@ describe('Tool Handlers', () => {
|
|||
tools: ['stable-diffusion'],
|
||||
functions: true,
|
||||
returnMap: true,
|
||||
useSpecs: true,
|
||||
});
|
||||
const structuredTool = await toolFunctions['stable-diffusion']();
|
||||
expect(structuredTool).toBeInstanceOf(StructuredSD);
|
||||
|
|
|
|||
1
api/cache/getLogStores.js
vendored
1
api/cache/getLogStores.js
vendored
|
|
@ -70,6 +70,7 @@ const namespaces = {
|
|||
[ViolationTypes.TTS_LIMIT]: createViolationInstance(ViolationTypes.TTS_LIMIT),
|
||||
[ViolationTypes.STT_LIMIT]: createViolationInstance(ViolationTypes.STT_LIMIT),
|
||||
[ViolationTypes.CONVO_ACCESS]: createViolationInstance(ViolationTypes.CONVO_ACCESS),
|
||||
[ViolationTypes.TOOL_CALL_LIMIT]: createViolationInstance(ViolationTypes.TOOL_CALL_LIMIT),
|
||||
[ViolationTypes.FILE_UPLOAD_LIMIT]: createViolationInstance(ViolationTypes.FILE_UPLOAD_LIMIT),
|
||||
[ViolationTypes.VERIFY_EMAIL_LIMIT]: createViolationInstance(ViolationTypes.VERIFY_EMAIL_LIMIT),
|
||||
[ViolationTypes.RESET_PASSWORD_LIMIT]: createViolationInstance(
|
||||
|
|
|
|||
|
|
@ -187,17 +187,33 @@ const debugTraverse = winston.format.printf(({ level, message, timestamp, ...met
|
|||
});
|
||||
|
||||
const jsonTruncateFormat = winston.format((info) => {
|
||||
const truncateLongStrings = (str, maxLength) => {
|
||||
return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
|
||||
};
|
||||
|
||||
const seen = new WeakSet();
|
||||
|
||||
const truncateObject = (obj) => {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Handle circular references
|
||||
if (seen.has(obj)) {
|
||||
return '[Circular]';
|
||||
}
|
||||
seen.add(obj);
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => truncateObject(item));
|
||||
}
|
||||
|
||||
const newObj = {};
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
newObj[key] = truncateLongStrings(value, 255);
|
||||
} else if (Array.isArray(value)) {
|
||||
newObj[key] = value.map(condenseArray);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
newObj[key] = truncateObject(value);
|
||||
} else {
|
||||
newObj[key] = value;
|
||||
newObj[key] = truncateObject(value);
|
||||
}
|
||||
});
|
||||
return newObj;
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ async function connectDb() {
|
|||
const disconnected = cached.conn && cached.conn?._readyState !== 1;
|
||||
if (!cached.promise || disconnected) {
|
||||
const opts = {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
bufferCommands: false,
|
||||
// useNewUrlParser: true,
|
||||
// useUnifiedTopology: true,
|
||||
// bufferMaxEntries: 0,
|
||||
// useFindAndModify: true,
|
||||
// useCreateIndex: true
|
||||
|
|
|
|||
|
|
@ -118,36 +118,43 @@ const addAgentResourceFile = async ({ agent_id, tool_resource, file_id }) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Removes a resource file id from an agent.
|
||||
* Removes multiple resource files from an agent in a single update.
|
||||
* @param {object} params
|
||||
* @param {ServerRequest} params.req
|
||||
* @param {string} params.agent_id
|
||||
* @param {string} params.tool_resource
|
||||
* @param {string} params.file_id
|
||||
* @param {Array<{tool_resource: string, file_id: string}>} params.files
|
||||
* @returns {Promise<Agent>} The updated agent.
|
||||
*/
|
||||
const removeAgentResourceFile = async ({ agent_id, tool_resource, file_id }) => {
|
||||
const removeAgentResourceFiles = async ({ agent_id, files }) => {
|
||||
const searchParameter = { id: agent_id };
|
||||
const agent = await getAgent(searchParameter);
|
||||
|
||||
if (!agent) {
|
||||
throw new Error('Agent not found for removing resource file');
|
||||
throw new Error('Agent not found for removing resource files');
|
||||
}
|
||||
|
||||
const tool_resources = agent.tool_resources || {};
|
||||
const tool_resources = { ...agent.tool_resources } || {};
|
||||
|
||||
if (tool_resources[tool_resource] && tool_resources[tool_resource].file_ids) {
|
||||
tool_resources[tool_resource].file_ids = tool_resources[tool_resource].file_ids.filter(
|
||||
(id) => id !== file_id,
|
||||
);
|
||||
|
||||
if (tool_resources[tool_resource].file_ids.length === 0) {
|
||||
delete tool_resources[tool_resource];
|
||||
const filesByResource = files.reduce((acc, { tool_resource, file_id }) => {
|
||||
if (!acc[tool_resource]) {
|
||||
acc[tool_resource] = new Set();
|
||||
}
|
||||
}
|
||||
acc[tool_resource].add(file_id);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
Object.entries(filesByResource).forEach(([resource, fileIds]) => {
|
||||
if (tool_resources[resource] && tool_resources[resource].file_ids) {
|
||||
tool_resources[resource].file_ids = tool_resources[resource].file_ids.filter(
|
||||
(id) => !fileIds.has(id),
|
||||
);
|
||||
|
||||
if (tool_resources[resource].file_ids.length === 0) {
|
||||
delete tool_resources[resource];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const updateData = { tool_resources };
|
||||
|
||||
return await updateAgent(searchParameter, updateData);
|
||||
};
|
||||
|
||||
|
|
@ -193,6 +200,7 @@ const getListAgents = async (searchParameter) => {
|
|||
avatar: 1,
|
||||
author: 1,
|
||||
projectIds: 1,
|
||||
description: 1,
|
||||
isCollaborative: 1,
|
||||
}).lean()
|
||||
).map((agent) => {
|
||||
|
|
@ -281,5 +289,5 @@ module.exports = {
|
|||
getListAgents,
|
||||
updateAgentProjects,
|
||||
addAgentResourceFile,
|
||||
removeAgentResourceFile,
|
||||
removeAgentResourceFiles,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,6 +15,19 @@ const searchConversation = async (conversationId) => {
|
|||
throw new Error('Error searching conversation');
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Searches for a conversation by conversationId and returns associated file ids.
|
||||
* @param {string} conversationId - The conversation's ID.
|
||||
* @returns {Promise<string[] | null>}
|
||||
*/
|
||||
const getConvoFiles = async (conversationId) => {
|
||||
try {
|
||||
return (await Conversation.findOne({ conversationId }, 'files').lean())?.files ?? [];
|
||||
} catch (error) {
|
||||
logger.error('[getConvoFiles] Error getting conversation files', error);
|
||||
throw new Error('Error getting conversation files');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a single conversation for a given user and conversation ID.
|
||||
|
|
@ -62,6 +75,7 @@ const deleteNullOrEmptyConversations = async () => {
|
|||
|
||||
module.exports = {
|
||||
Conversation,
|
||||
getConvoFiles,
|
||||
searchConversation,
|
||||
deleteNullOrEmptyConversations,
|
||||
/**
|
||||
|
|
@ -82,6 +96,7 @@ module.exports = {
|
|||
update.conversationId = newConversationId;
|
||||
}
|
||||
|
||||
/** Note: the resulting Model object is necessary for Meilisearch operations */
|
||||
const conversation = await Conversation.findOneAndUpdate(
|
||||
{ conversationId, user: req.user.id },
|
||||
update,
|
||||
|
|
|
|||
|
|
@ -265,6 +265,26 @@ async function getMessages(filter, select) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single message from the database.
|
||||
* @async
|
||||
* @function getMessage
|
||||
* @param {{ user: string, messageId: string }} params - The search parameters
|
||||
* @returns {Promise<TMessage | null>} The message that matches the criteria or null if not found
|
||||
* @throws {Error} If there is an error in retrieving the message
|
||||
*/
|
||||
async function getMessage({ user, messageId }) {
|
||||
try {
|
||||
return await Message.findOne({
|
||||
user,
|
||||
messageId,
|
||||
}).lean();
|
||||
} catch (err) {
|
||||
logger.error('Error getting message:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes messages from the database.
|
||||
*
|
||||
|
|
@ -292,5 +312,6 @@ module.exports = {
|
|||
updateMessage,
|
||||
deleteMessagesSince,
|
||||
getMessages,
|
||||
getMessage,
|
||||
deleteMessages,
|
||||
};
|
||||
|
|
|
|||
96
api/models/ToolCall.js
Normal file
96
api/models/ToolCall.js
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
const ToolCall = require('./schema/toolCallSchema');
|
||||
|
||||
/**
|
||||
* Create a new tool call
|
||||
* @param {ToolCallData} toolCallData - The tool call data
|
||||
* @returns {Promise<ToolCallData>} The created tool call document
|
||||
*/
|
||||
async function createToolCall(toolCallData) {
|
||||
try {
|
||||
return await ToolCall.create(toolCallData);
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating tool call: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a tool call by ID
|
||||
* @param {string} id - The tool call document ID
|
||||
* @returns {Promise<ToolCallData|null>} The tool call document or null if not found
|
||||
*/
|
||||
async function getToolCallById(id) {
|
||||
try {
|
||||
return await ToolCall.findById(id).lean();
|
||||
} catch (error) {
|
||||
throw new Error(`Error fetching tool call: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool calls by message ID and user
|
||||
* @param {string} messageId - The message ID
|
||||
* @param {string} userId - The user's ObjectId
|
||||
* @returns {Promise<Array>} Array of tool call documents
|
||||
*/
|
||||
async function getToolCallsByMessage(messageId, userId) {
|
||||
try {
|
||||
return await ToolCall.find({ messageId, user: userId }).lean();
|
||||
} catch (error) {
|
||||
throw new Error(`Error fetching tool calls: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool calls by conversation ID and user
|
||||
* @param {string} conversationId - The conversation ID
|
||||
* @param {string} userId - The user's ObjectId
|
||||
* @returns {Promise<ToolCallData[]>} Array of tool call documents
|
||||
*/
|
||||
async function getToolCallsByConvo(conversationId, userId) {
|
||||
try {
|
||||
return await ToolCall.find({ conversationId, user: userId }).lean();
|
||||
} catch (error) {
|
||||
throw new Error(`Error fetching tool calls: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a tool call
|
||||
* @param {string} id - The tool call document ID
|
||||
* @param {Partial<ToolCallData>} updateData - The data to update
|
||||
* @returns {Promise<ToolCallData|null>} The updated tool call document or null if not found
|
||||
*/
|
||||
async function updateToolCall(id, updateData) {
|
||||
try {
|
||||
return await ToolCall.findByIdAndUpdate(id, updateData, { new: true }).lean();
|
||||
} catch (error) {
|
||||
throw new Error(`Error updating tool call: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tool call
|
||||
* @param {string} userId - The related user's ObjectId
|
||||
* @param {string} [conversationId] - The tool call conversation ID
|
||||
* @returns {Promise<{ ok?: number; n?: number; deletedCount?: number }>} The result of the delete operation
|
||||
*/
|
||||
async function deleteToolCalls(userId, conversationId) {
|
||||
try {
|
||||
const query = { user: userId };
|
||||
if (conversationId) {
|
||||
query.conversationId = conversationId;
|
||||
}
|
||||
return await ToolCall.deleteMany(query);
|
||||
} catch (error) {
|
||||
throw new Error(`Error deleting tool call: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createToolCall,
|
||||
updateToolCall,
|
||||
deleteToolCalls,
|
||||
getToolCallById,
|
||||
getToolCallsByConvo,
|
||||
getToolCallsByMessage,
|
||||
};
|
||||
|
|
@ -18,6 +18,7 @@ const {
|
|||
updateFileUsage,
|
||||
} = require('./File');
|
||||
const {
|
||||
getMessage,
|
||||
getMessages,
|
||||
saveMessage,
|
||||
recordMessage,
|
||||
|
|
@ -51,6 +52,7 @@ module.exports = {
|
|||
getFiles,
|
||||
updateFileUsage,
|
||||
|
||||
getMessage,
|
||||
getMessages,
|
||||
saveMessage,
|
||||
recordMessage,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,15 @@ const agentSchema = mongoose.Schema(
|
|||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
hide_sequential_outputs: {
|
||||
type: Boolean,
|
||||
},
|
||||
end_after_tools: {
|
||||
type: Boolean,
|
||||
},
|
||||
agent_ids: {
|
||||
type: [String],
|
||||
},
|
||||
isCollaborative: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ const assistantSchema = mongoose.Schema(
|
|||
},
|
||||
file_ids: { type: [String], default: undefined },
|
||||
actions: { type: [String], default: undefined },
|
||||
append_current_datetime: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ const convoSchema = mongoose.Schema(
|
|||
type: mongoose.Schema.Types.Mixed,
|
||||
},
|
||||
...conversationPreset,
|
||||
agent_id: {
|
||||
type: String,
|
||||
},
|
||||
// for bingAI only
|
||||
bingConversationId: {
|
||||
type: String,
|
||||
|
|
@ -47,6 +50,9 @@ const convoSchema = mongoose.Schema(
|
|||
default: [],
|
||||
meiliIndex: true,
|
||||
},
|
||||
files: {
|
||||
type: [String],
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ const conversationPreset = {
|
|||
imageDetail: {
|
||||
type: String,
|
||||
},
|
||||
/* agents */
|
||||
agent_id: {
|
||||
type: String,
|
||||
},
|
||||
/* assistants */
|
||||
assistant_id: {
|
||||
type: String,
|
||||
|
|
|
|||
54
api/models/schema/toolCallSchema.js
Normal file
54
api/models/schema/toolCallSchema.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* @typedef {Object} ToolCallData
|
||||
* @property {string} conversationId - The ID of the conversation
|
||||
* @property {string} messageId - The ID of the message
|
||||
* @property {string} toolId - The ID of the tool
|
||||
* @property {string | ObjectId} user - The user's ObjectId
|
||||
* @property {unknown} [result] - Optional result data
|
||||
* @property {TAttachment[]} [attachments] - Optional attachments data
|
||||
* @property {number} [blockIndex] - Optional code block index
|
||||
* @property {number} [partIndex] - Optional part index
|
||||
*/
|
||||
|
||||
/** @type {MongooseSchema<ToolCallData>} */
|
||||
const toolCallSchema = mongoose.Schema(
|
||||
{
|
||||
conversationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
messageId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
toolId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
result: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
},
|
||||
attachments: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
},
|
||||
blockIndex: {
|
||||
type: Number,
|
||||
},
|
||||
partIndex: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
toolCallSchema.index({ messageId: 1, user: 1 });
|
||||
toolCallSchema.index({ conversationId: 1, user: 1 });
|
||||
|
||||
module.exports = mongoose.model('ToolCall', toolCallSchema);
|
||||
|
|
@ -30,6 +30,9 @@ const bedrockValues = {
|
|||
'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 },
|
||||
'amazon.nova-micro-v1:0': { prompt: 0.035, completion: 0.14 },
|
||||
'amazon.nova-lite-v1:0': { prompt: 0.06, completion: 0.24 },
|
||||
'amazon.nova-pro-v1:0': { prompt: 0.8, completion: 3.2 },
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -56,8 +59,8 @@ const tokenValues = Object.assign(
|
|||
'claude-3-sonnet': { prompt: 3, completion: 15 },
|
||||
'claude-3-5-sonnet': { prompt: 3, completion: 15 },
|
||||
'claude-3.5-sonnet': { prompt: 3, completion: 15 },
|
||||
'claude-3-5-haiku': { prompt: 1, completion: 5 },
|
||||
'claude-3.5-haiku': { prompt: 1, completion: 5 },
|
||||
'claude-3-5-haiku': { prompt: 0.8, completion: 4 },
|
||||
'claude-3.5-haiku': { prompt: 0.8, completion: 4 },
|
||||
'claude-3-haiku': { prompt: 0.25, completion: 1.25 },
|
||||
'claude-2.1': { prompt: 8, completion: 24 },
|
||||
'claude-2': { prompt: 8, completion: 24 },
|
||||
|
|
@ -68,6 +71,7 @@ const tokenValues = Object.assign(
|
|||
/* cohere doesn't have rates for the older command models,
|
||||
so this was from https://artificialanalysis.ai/models/command-light/providers */
|
||||
command: { prompt: 0.38, completion: 0.38 },
|
||||
'gemini-2.0': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
|
||||
'gemini-1.5': { prompt: 7, completion: 21 }, // May 2nd, 2024 pricing
|
||||
gemini: { prompt: 0.5, completion: 1.5 }, // May 2nd, 2024 pricing
|
||||
},
|
||||
|
|
@ -83,8 +87,8 @@ const tokenValues = Object.assign(
|
|||
const cacheTokenValues = {
|
||||
'claude-3.5-sonnet': { write: 3.75, read: 0.3 },
|
||||
'claude-3-5-sonnet': { write: 3.75, read: 0.3 },
|
||||
'claude-3.5-haiku': { write: 1.25, read: 0.1 },
|
||||
'claude-3-5-haiku': { write: 1.25, read: 0.1 },
|
||||
'claude-3.5-haiku': { write: 1, read: 0.08 },
|
||||
'claude-3-5-haiku': { write: 1, read: 0.08 },
|
||||
'claude-3-haiku': { write: 0.3, read: 0.03 },
|
||||
};
|
||||
|
||||
|
|
@ -208,4 +212,11 @@ const getCacheMultiplier = ({ valueKey, cacheType, model, endpoint, endpointToke
|
|||
return cacheTokenValues[valueKey]?.[cacheType] ?? null;
|
||||
};
|
||||
|
||||
module.exports = { tokenValues, getValueKey, getMultiplier, getCacheMultiplier, defaultRate };
|
||||
module.exports = {
|
||||
tokenValues,
|
||||
getValueKey,
|
||||
getMultiplier,
|
||||
getCacheMultiplier,
|
||||
defaultRate,
|
||||
cacheTokenValues,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const {
|
|||
tokenValues,
|
||||
getValueKey,
|
||||
getMultiplier,
|
||||
cacheTokenValues,
|
||||
getCacheMultiplier,
|
||||
} = require('./tx');
|
||||
|
||||
|
|
@ -211,6 +212,7 @@ describe('getMultiplier', () => {
|
|||
|
||||
describe('AWS Bedrock Model Tests', () => {
|
||||
const awsModels = [
|
||||
'anthropic.claude-3-5-haiku-20241022-v1:0',
|
||||
'anthropic.claude-3-haiku-20240307-v1:0',
|
||||
'anthropic.claude-3-sonnet-20240229-v1:0',
|
||||
'anthropic.claude-3-opus-20240229-v1:0',
|
||||
|
|
@ -237,6 +239,9 @@ describe('AWS Bedrock Model Tests', () => {
|
|||
'ai21.j2-ultra-v1',
|
||||
'amazon.titan-text-lite-v1',
|
||||
'amazon.titan-text-express-v1',
|
||||
'amazon.nova-micro-v1:0',
|
||||
'amazon.nova-lite-v1:0',
|
||||
'amazon.nova-pro-v1:0',
|
||||
];
|
||||
|
||||
it('should return the correct prompt multipliers for all models', () => {
|
||||
|
|
@ -260,12 +265,24 @@ describe('AWS Bedrock Model Tests', () => {
|
|||
|
||||
describe('getCacheMultiplier', () => {
|
||||
it('should return the correct cache multiplier for a given valueKey and cacheType', () => {
|
||||
expect(getCacheMultiplier({ valueKey: 'claude-3-5-sonnet', cacheType: 'write' })).toBe(3.75);
|
||||
expect(getCacheMultiplier({ valueKey: 'claude-3-5-sonnet', cacheType: 'read' })).toBe(0.3);
|
||||
expect(getCacheMultiplier({ valueKey: 'claude-3-5-haiku', cacheType: 'write' })).toBe(1.25);
|
||||
expect(getCacheMultiplier({ valueKey: 'claude-3-5-haiku', cacheType: 'read' })).toBe(0.1);
|
||||
expect(getCacheMultiplier({ valueKey: 'claude-3-haiku', cacheType: 'write' })).toBe(0.3);
|
||||
expect(getCacheMultiplier({ valueKey: 'claude-3-haiku', cacheType: 'read' })).toBe(0.03);
|
||||
expect(getCacheMultiplier({ valueKey: 'claude-3-5-sonnet', cacheType: 'write' })).toBe(
|
||||
cacheTokenValues['claude-3-5-sonnet'].write,
|
||||
);
|
||||
expect(getCacheMultiplier({ valueKey: 'claude-3-5-sonnet', cacheType: 'read' })).toBe(
|
||||
cacheTokenValues['claude-3-5-sonnet'].read,
|
||||
);
|
||||
expect(getCacheMultiplier({ valueKey: 'claude-3-5-haiku', cacheType: 'write' })).toBe(
|
||||
cacheTokenValues['claude-3-5-haiku'].write,
|
||||
);
|
||||
expect(getCacheMultiplier({ valueKey: 'claude-3-5-haiku', cacheType: 'read' })).toBe(
|
||||
cacheTokenValues['claude-3-5-haiku'].read,
|
||||
);
|
||||
expect(getCacheMultiplier({ valueKey: 'claude-3-haiku', cacheType: 'write' })).toBe(
|
||||
cacheTokenValues['claude-3-haiku'].write,
|
||||
);
|
||||
expect(getCacheMultiplier({ valueKey: 'claude-3-haiku', cacheType: 'read' })).toBe(
|
||||
cacheTokenValues['claude-3-haiku'].read,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null if cacheType is provided but not found in cacheTokenValues', () => {
|
||||
|
|
|
|||
|
|
@ -39,12 +39,12 @@
|
|||
"@google/generative-ai": "^0.21.0",
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@keyv/redis": "^2.8.1",
|
||||
"@langchain/community": "^0.3.13",
|
||||
"@langchain/core": "^0.3.17",
|
||||
"@langchain/community": "^0.3.14",
|
||||
"@langchain/core": "^0.3.18",
|
||||
"@langchain/google-genai": "^0.1.4",
|
||||
"@langchain/google-vertexai": "^0.1.2",
|
||||
"@langchain/textsplitters": "^0.1.0",
|
||||
"@librechat/agents": "^1.7.7",
|
||||
"@librechat/agents": "^1.8.5",
|
||||
"axios": "^1.7.7",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"dedent": "^1.5.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.21.1",
|
||||
"express": "^4.21.2",
|
||||
"express-mongo-sanitize": "^2.2.0",
|
||||
"express-rate-limit": "^7.4.1",
|
||||
"express-session": "^1.18.1",
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
"meilisearch": "^0.38.0",
|
||||
"mime": "^3.0.0",
|
||||
"module-alias": "^2.2.3",
|
||||
"mongoose": "^7.3.3",
|
||||
"mongoose": "^8.8.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nanoid": "^3.3.7",
|
||||
"nodejs-gpt": "^1.37.4",
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
|||
},
|
||||
};
|
||||
|
||||
/** @type {TMessage} */
|
||||
let response = await client.sendMessage(text, messageOptions);
|
||||
response.endpoint = endpointOption.endpoint;
|
||||
|
||||
|
|
@ -150,11 +151,13 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
|||
});
|
||||
res.end();
|
||||
|
||||
await saveMessage(
|
||||
req,
|
||||
{ ...response, user },
|
||||
{ context: 'api/server/controllers/AskController.js - response end' },
|
||||
);
|
||||
if (!client.savedMessageIds.has(response.messageId)) {
|
||||
await saveMessage(
|
||||
req,
|
||||
{ ...response, user },
|
||||
{ context: 'api/server/controllers/AskController.js - response end' },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!client.skipSaveUserMessage) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,15 @@ async function endpointController(req, res) {
|
|||
capabilities,
|
||||
};
|
||||
}
|
||||
if (mergedConfig[EModelEndpoint.agents] && req.app.locals?.[EModelEndpoint.agents]) {
|
||||
const { disableBuilder, capabilities, ..._rest } = req.app.locals[EModelEndpoint.agents];
|
||||
|
||||
mergedConfig[EModelEndpoint.agents] = {
|
||||
...mergedConfig[EModelEndpoint.agents],
|
||||
disableBuilder,
|
||||
capabilities,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
mergedConfig[EModelEndpoint.azureAssistants] &&
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const { updateUserPluginsService, deleteUserKey } = require('~/server/services/U
|
|||
const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService');
|
||||
const { processDeleteRequest } = require('~/server/services/Files/process');
|
||||
const { deleteAllSharedLinks } = require('~/models/Share');
|
||||
const { deleteToolCalls } = require('~/models/ToolCall');
|
||||
const { Transaction } = require('~/models/Transaction');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
|
|
@ -123,6 +124,7 @@ const deleteUserController = async (req, res) => {
|
|||
await deleteAllSharedLinks(user.id); // delete user shared links
|
||||
await deleteUserFiles(req); // delete user files
|
||||
await deleteFiles(null, user.id); // delete database files in case of orphaned files from previous steps
|
||||
await deleteToolCalls(user.id); // delete user tool calls
|
||||
/* TODO: queue job for cleaning actions and assistants of non-existant users */
|
||||
logger.info(`User deleted account. Email: ${user.email} ID: ${user.id}`);
|
||||
res.status(200).send({ message: 'User deleted' });
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const { Tools } = require('librechat-data-provider');
|
||||
const { Tools, StepTypes, imageGenTools } = require('librechat-data-provider');
|
||||
const {
|
||||
EnvVar,
|
||||
GraphEvents,
|
||||
|
|
@ -57,6 +57,9 @@ class ModelEndHandler {
|
|||
}
|
||||
|
||||
const usage = data?.output?.usage_metadata;
|
||||
if (metadata?.model) {
|
||||
usage.model = metadata.model;
|
||||
}
|
||||
|
||||
if (usage) {
|
||||
this.collectedUsage.push(usage);
|
||||
|
|
@ -89,9 +92,27 @@ function getDefaultHandlers({ res, aggregateContent, toolEndCallback, collectedU
|
|||
* Handle ON_RUN_STEP event.
|
||||
* @param {string} event - The event name.
|
||||
* @param {StreamEventData} data - The event data.
|
||||
* @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata.
|
||||
*/
|
||||
handle: (event, data) => {
|
||||
sendEvent(res, { event, data });
|
||||
handle: (event, data, metadata) => {
|
||||
if (data?.stepDetails.type === StepTypes.TOOL_CALLS) {
|
||||
sendEvent(res, { event, data });
|
||||
} else if (metadata?.last_agent_index === metadata?.agent_index) {
|
||||
sendEvent(res, { event, data });
|
||||
} else if (!metadata?.hide_sequential_outputs) {
|
||||
sendEvent(res, { event, data });
|
||||
} else {
|
||||
const agentName = metadata?.name ?? 'Agent';
|
||||
const isToolCall = data?.stepDetails.type === StepTypes.TOOL_CALLS;
|
||||
const action = isToolCall ? 'performing a task...' : 'thinking...';
|
||||
sendEvent(res, {
|
||||
event: 'on_agent_update',
|
||||
data: {
|
||||
runId: metadata?.run_id,
|
||||
message: `${agentName} is ${action}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
aggregateContent({ event, data });
|
||||
},
|
||||
},
|
||||
|
|
@ -100,9 +121,16 @@ function getDefaultHandlers({ res, aggregateContent, toolEndCallback, collectedU
|
|||
* Handle ON_RUN_STEP_DELTA event.
|
||||
* @param {string} event - The event name.
|
||||
* @param {StreamEventData} data - The event data.
|
||||
* @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata.
|
||||
*/
|
||||
handle: (event, data) => {
|
||||
sendEvent(res, { event, data });
|
||||
handle: (event, data, metadata) => {
|
||||
if (data?.delta.type === StepTypes.TOOL_CALLS) {
|
||||
sendEvent(res, { event, data });
|
||||
} else if (metadata?.last_agent_index === metadata?.agent_index) {
|
||||
sendEvent(res, { event, data });
|
||||
} else if (!metadata?.hide_sequential_outputs) {
|
||||
sendEvent(res, { event, data });
|
||||
}
|
||||
aggregateContent({ event, data });
|
||||
},
|
||||
},
|
||||
|
|
@ -111,9 +139,16 @@ function getDefaultHandlers({ res, aggregateContent, toolEndCallback, collectedU
|
|||
* Handle ON_RUN_STEP_COMPLETED event.
|
||||
* @param {string} event - The event name.
|
||||
* @param {StreamEventData & { result: ToolEndData }} data - The event data.
|
||||
* @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata.
|
||||
*/
|
||||
handle: (event, data) => {
|
||||
sendEvent(res, { event, data });
|
||||
handle: (event, data, metadata) => {
|
||||
if (data?.result != null) {
|
||||
sendEvent(res, { event, data });
|
||||
} else if (metadata?.last_agent_index === metadata?.agent_index) {
|
||||
sendEvent(res, { event, data });
|
||||
} else if (!metadata?.hide_sequential_outputs) {
|
||||
sendEvent(res, { event, data });
|
||||
}
|
||||
aggregateContent({ event, data });
|
||||
},
|
||||
},
|
||||
|
|
@ -122,9 +157,14 @@ function getDefaultHandlers({ res, aggregateContent, toolEndCallback, collectedU
|
|||
* Handle ON_MESSAGE_DELTA event.
|
||||
* @param {string} event - The event name.
|
||||
* @param {StreamEventData} data - The event data.
|
||||
* @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata.
|
||||
*/
|
||||
handle: (event, data) => {
|
||||
sendEvent(res, { event, data });
|
||||
handle: (event, data, metadata) => {
|
||||
if (metadata?.last_agent_index === metadata?.agent_index) {
|
||||
sendEvent(res, { event, data });
|
||||
} else if (!metadata?.hide_sequential_outputs) {
|
||||
sendEvent(res, { event, data });
|
||||
}
|
||||
aggregateContent({ event, data });
|
||||
},
|
||||
},
|
||||
|
|
@ -151,16 +191,41 @@ function createToolEndCallback({ req, res, artifactPromises }) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (imageGenTools.has(output.name) && output.artifact) {
|
||||
artifactPromises.push(
|
||||
(async () => {
|
||||
const fileMetadata = Object.assign(output.artifact, {
|
||||
messageId: metadata.run_id,
|
||||
toolCallId: output.tool_call_id,
|
||||
conversationId: metadata.thread_id,
|
||||
});
|
||||
if (!res.headersSent) {
|
||||
return fileMetadata;
|
||||
}
|
||||
|
||||
if (!fileMetadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
res.write(`event: attachment\ndata: ${JSON.stringify(fileMetadata)}\n\n`);
|
||||
return fileMetadata;
|
||||
})().catch((error) => {
|
||||
logger.error('Error processing code output:', error);
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (output.name !== Tools.execute_code) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { tool_call_id, artifact } = output;
|
||||
if (!artifact.files) {
|
||||
if (!output.artifact.files) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of artifact.files) {
|
||||
for (const file of output.artifact.files) {
|
||||
const { id, name } = file;
|
||||
artifactPromises.push(
|
||||
(async () => {
|
||||
|
|
@ -173,10 +238,10 @@ function createToolEndCallback({ req, res, artifactPromises }) {
|
|||
id,
|
||||
name,
|
||||
apiKey: result[EnvVar.CODE_API_KEY],
|
||||
toolCallId: tool_call_id,
|
||||
messageId: metadata.run_id,
|
||||
session_id: artifact.session_id,
|
||||
toolCallId: output.tool_call_id,
|
||||
conversationId: metadata.thread_id,
|
||||
session_id: output.artifact.session_id,
|
||||
});
|
||||
if (!res.headersSent) {
|
||||
return fileMetadata;
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ const {
|
|||
Constants,
|
||||
VisionModes,
|
||||
openAISchema,
|
||||
ContentTypes,
|
||||
EModelEndpoint,
|
||||
KnownEndpoints,
|
||||
anthropicSchema,
|
||||
isAgentsEndpoint,
|
||||
bedrockOutputParser,
|
||||
removeNullishValues,
|
||||
} = require('librechat-data-provider');
|
||||
|
|
@ -30,10 +32,10 @@ const {
|
|||
createContextHandlers,
|
||||
} = require('~/app/clients/prompts');
|
||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||
const { getBufferString, HumanMessage } = require('@langchain/core/messages');
|
||||
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');
|
||||
|
||||
|
|
@ -48,6 +50,12 @@ const providerParsers = {
|
|||
|
||||
const legacyContentEndpoints = new Set([KnownEndpoints.groq, KnownEndpoints.deepseek]);
|
||||
|
||||
const noSystemModelRegex = [/\bo1\b/gi];
|
||||
|
||||
// const { processMemory, memoryInstructions } = require('~/server/services/Endpoints/agents/memory');
|
||||
// const { getFormattedMemories } = require('~/models/Memory');
|
||||
// const { getCurrentDateTime } = require('~/utils');
|
||||
|
||||
class AgentClient extends BaseClient {
|
||||
constructor(options = {}) {
|
||||
super(null, options);
|
||||
|
|
@ -62,15 +70,15 @@ class AgentClient extends BaseClient {
|
|||
this.run;
|
||||
|
||||
const {
|
||||
agentConfigs,
|
||||
contentParts,
|
||||
collectedUsage,
|
||||
artifactPromises,
|
||||
maxContextTokens,
|
||||
modelOptions = {},
|
||||
...clientOptions
|
||||
} = options;
|
||||
|
||||
this.modelOptions = modelOptions;
|
||||
this.agentConfigs = agentConfigs;
|
||||
this.maxContextTokens = maxContextTokens;
|
||||
/** @type {MessageContentComplex[]} */
|
||||
this.contentParts = contentParts;
|
||||
|
|
@ -80,6 +88,8 @@ class AgentClient extends BaseClient {
|
|||
this.artifactPromises = artifactPromises;
|
||||
/** @type {AgentClientOptions} */
|
||||
this.options = Object.assign({ endpoint: options.endpoint }, clientOptions);
|
||||
/** @type {string} */
|
||||
this.model = this.options.agent.model_parameters.model;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -169,7 +179,7 @@ class AgentClient extends BaseClient {
|
|||
: {};
|
||||
|
||||
if (parseOptions) {
|
||||
runOptions = parseOptions(this.modelOptions);
|
||||
runOptions = parseOptions(this.options.agent.model_parameters);
|
||||
}
|
||||
|
||||
return removeNullishValues(
|
||||
|
|
@ -224,7 +234,28 @@ class AgentClient extends BaseClient {
|
|||
let promptTokens;
|
||||
|
||||
/** @type {string} */
|
||||
let systemContent = `${instructions ?? ''}${additional_instructions ?? ''}`;
|
||||
let systemContent = [instructions ?? '', additional_instructions ?? '']
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
.trim();
|
||||
// this.systemMessage = getCurrentDateTime();
|
||||
// const { withKeys, withoutKeys } = await getFormattedMemories({
|
||||
// userId: this.options.req.user.id,
|
||||
// });
|
||||
// processMemory({
|
||||
// userId: this.options.req.user.id,
|
||||
// message: this.options.req.body.text,
|
||||
// parentMessageId,
|
||||
// memory: withKeys,
|
||||
// thread_id: this.conversationId,
|
||||
// }).catch((error) => {
|
||||
// logger.error('Memory Agent failed to process memory', error);
|
||||
// });
|
||||
|
||||
// this.systemMessage += '\n\n' + memoryInstructions;
|
||||
// if (withoutKeys) {
|
||||
// this.systemMessage += `\n\n# Existing memory about the user:\n${withoutKeys}`;
|
||||
// }
|
||||
|
||||
if (this.options.attachments) {
|
||||
const attachments = await this.options.attachments;
|
||||
|
|
@ -245,7 +276,8 @@ class AgentClient extends BaseClient {
|
|||
this.options.attachments = files;
|
||||
}
|
||||
|
||||
if (this.message_file_map) {
|
||||
/** Note: Bedrock uses legacy RAG API handling */
|
||||
if (this.message_file_map && !isAgentsEndpoint(this.options.endpoint)) {
|
||||
this.contextHandlers = createContextHandlers(
|
||||
this.options.req,
|
||||
orderedMessages[orderedMessages.length - 1].text,
|
||||
|
|
@ -319,7 +351,6 @@ class AgentClient extends BaseClient {
|
|||
|
||||
/** @type {sendCompletion} */
|
||||
async sendCompletion(payload, opts = {}) {
|
||||
this.modelOptions.user = this.user;
|
||||
await this.chatCompletion({
|
||||
payload,
|
||||
onProgress: opts.onProgress,
|
||||
|
|
@ -339,10 +370,10 @@ class AgentClient extends BaseClient {
|
|||
await spendTokens(
|
||||
{
|
||||
context,
|
||||
model: model ?? this.modelOptions.model,
|
||||
conversationId: this.conversationId,
|
||||
user: this.user ?? this.options.req.user?.id,
|
||||
endpointTokenConfig: this.options.endpointTokenConfig,
|
||||
model: usage.model ?? model ?? this.model ?? this.options.agent.model_parameters.model,
|
||||
},
|
||||
{ promptTokens: usage.input_tokens, completionTokens: usage.output_tokens },
|
||||
);
|
||||
|
|
@ -457,43 +488,190 @@ class AgentClient extends BaseClient {
|
|||
// });
|
||||
// }
|
||||
|
||||
const run = await createRun({
|
||||
req: this.options.req,
|
||||
agent: this.options.agent,
|
||||
tools: this.options.tools,
|
||||
runId: this.responseMessageId,
|
||||
modelOptions: this.modelOptions,
|
||||
customHandlers: this.options.eventHandlers,
|
||||
});
|
||||
|
||||
const config = {
|
||||
configurable: {
|
||||
thread_id: this.conversationId,
|
||||
last_agent_index: this.agentConfigs?.size ?? 0,
|
||||
hide_sequential_outputs: this.options.agent.hide_sequential_outputs,
|
||||
},
|
||||
signal: abortController.signal,
|
||||
streamMode: 'values',
|
||||
version: 'v2',
|
||||
};
|
||||
|
||||
if (!run) {
|
||||
throw new Error('Failed to create run');
|
||||
}
|
||||
|
||||
this.run = run;
|
||||
|
||||
const messages = formatAgentMessages(payload);
|
||||
const initialMessages = formatAgentMessages(payload);
|
||||
if (legacyContentEndpoints.has(this.options.agent.endpoint)) {
|
||||
formatContentStrings(messages);
|
||||
formatContentStrings(initialMessages);
|
||||
}
|
||||
await run.processStream({ messages }, config, {
|
||||
[Callback.TOOL_ERROR]: (graph, error, toolId) => {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #chatCompletion] Tool Error',
|
||||
error,
|
||||
toolId,
|
||||
);
|
||||
},
|
||||
|
||||
/** @type {ReturnType<createRun>} */
|
||||
let run;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Agent} agent
|
||||
* @param {BaseMessage[]} messages
|
||||
* @param {number} [i]
|
||||
* @param {TMessageContentParts[]} [contentData]
|
||||
*/
|
||||
const runAgent = async (agent, messages, i = 0, contentData = []) => {
|
||||
config.configurable.model = agent.model_parameters.model;
|
||||
if (i > 0) {
|
||||
this.model = agent.model_parameters.model;
|
||||
}
|
||||
config.configurable.agent_id = agent.id;
|
||||
config.configurable.name = agent.name;
|
||||
config.configurable.agent_index = i;
|
||||
const noSystemMessages = noSystemModelRegex.some((regex) =>
|
||||
agent.model_parameters.model.match(regex),
|
||||
);
|
||||
|
||||
const systemMessage = Object.values(agent.toolContextMap ?? {})
|
||||
.join('\n')
|
||||
.trim();
|
||||
|
||||
let systemContent = [
|
||||
systemMessage,
|
||||
agent.instructions ?? '',
|
||||
i !== 0 ? agent.additional_instructions ?? '' : '',
|
||||
]
|
||||
.join('\n')
|
||||
.trim();
|
||||
|
||||
if (noSystemMessages === true) {
|
||||
agent.instructions = undefined;
|
||||
agent.additional_instructions = undefined;
|
||||
} else {
|
||||
agent.instructions = systemContent;
|
||||
agent.additional_instructions = undefined;
|
||||
}
|
||||
|
||||
if (noSystemMessages === true && systemContent?.length) {
|
||||
let latestMessage = messages.pop().content;
|
||||
if (typeof latestMessage !== 'string') {
|
||||
latestMessage = latestMessage[0].text;
|
||||
}
|
||||
latestMessage = [systemContent, latestMessage].join('\n');
|
||||
messages.push(new HumanMessage(latestMessage));
|
||||
}
|
||||
|
||||
run = await createRun({
|
||||
agent,
|
||||
req: this.options.req,
|
||||
runId: this.responseMessageId,
|
||||
signal: abortController.signal,
|
||||
customHandlers: this.options.eventHandlers,
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
throw new Error('Failed to create run');
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
this.run = run;
|
||||
}
|
||||
|
||||
if (contentData.length) {
|
||||
run.Graph.contentData = contentData;
|
||||
}
|
||||
|
||||
await run.processStream({ messages }, config, {
|
||||
keepContent: i !== 0,
|
||||
callbacks: {
|
||||
[Callback.TOOL_ERROR]: (graph, error, toolId) => {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #chatCompletion] Tool Error',
|
||||
error,
|
||||
toolId,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
await runAgent(this.options.agent, initialMessages);
|
||||
|
||||
let finalContentStart = 0;
|
||||
if (this.agentConfigs && this.agentConfigs.size > 0) {
|
||||
let latestMessage = initialMessages.pop().content;
|
||||
if (typeof latestMessage !== 'string') {
|
||||
latestMessage = latestMessage[0].text;
|
||||
}
|
||||
let i = 1;
|
||||
let runMessages = [];
|
||||
|
||||
const lastFiveMessages = initialMessages.slice(-5);
|
||||
for (const [agentId, agent] of this.agentConfigs) {
|
||||
if (abortController.signal.aborted === true) {
|
||||
break;
|
||||
}
|
||||
const currentRun = await run;
|
||||
|
||||
if (
|
||||
i === this.agentConfigs.size &&
|
||||
config.configurable.hide_sequential_outputs === true
|
||||
) {
|
||||
const content = this.contentParts.filter(
|
||||
(part) => part.type === ContentTypes.TOOL_CALL,
|
||||
);
|
||||
|
||||
this.options.res.write(
|
||||
`event: message\ndata: ${JSON.stringify({
|
||||
event: 'on_content_update',
|
||||
data: {
|
||||
runId: this.responseMessageId,
|
||||
content,
|
||||
},
|
||||
})}\n\n`,
|
||||
);
|
||||
}
|
||||
const _runMessages = currentRun.Graph.getRunMessages();
|
||||
finalContentStart = this.contentParts.length;
|
||||
runMessages = runMessages.concat(_runMessages);
|
||||
const contentData = currentRun.Graph.contentData.slice();
|
||||
const bufferString = getBufferString([new HumanMessage(latestMessage), ...runMessages]);
|
||||
if (i === this.agentConfigs.size) {
|
||||
logger.debug(`SEQUENTIAL AGENTS: Last buffer string:\n${bufferString}`);
|
||||
}
|
||||
try {
|
||||
const contextMessages = [];
|
||||
for (const message of lastFiveMessages) {
|
||||
const messageType = message._getType();
|
||||
if (
|
||||
(!agent.tools || agent.tools.length === 0) &&
|
||||
(messageType === 'tool' || (message.tool_calls?.length ?? 0) > 0)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
contextMessages.push(message);
|
||||
}
|
||||
const currentMessages = [...contextMessages, new HumanMessage(bufferString)];
|
||||
await runAgent(agent, currentMessages, i, contentData);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[api/server/controllers/agents/client.js #chatCompletion] Error running agent ${agentId} (${i})`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.configurable.hide_sequential_outputs !== true) {
|
||||
finalContentStart = 0;
|
||||
}
|
||||
|
||||
this.contentParts = this.contentParts.filter((part, index) => {
|
||||
// Include parts that are either:
|
||||
// 1. At or after the finalContentStart index
|
||||
// 2. Of type tool_call
|
||||
// 3. Have tool_call_ids property
|
||||
return (
|
||||
index >= finalContentStart || part.type === ContentTypes.TOOL_CALL || part.tool_call_ids
|
||||
);
|
||||
});
|
||||
|
||||
this.recordCollectedUsage({ context: 'message' }).catch((err) => {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #chatCompletion] Error recording collected usage',
|
||||
|
|
@ -586,7 +764,7 @@ class AgentClient extends BaseClient {
|
|||
}
|
||||
|
||||
getEncoding() {
|
||||
return this.modelOptions.model?.includes('gpt-4o') ? 'o200k_base' : 'cl100k_base';
|
||||
return this.model?.includes('gpt-4o') ? 'o200k_base' : 'cl100k_base';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -94,8 +94,14 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
|||
conversation.title =
|
||||
conversation && !conversation.title ? null : conversation?.title || 'New Chat';
|
||||
|
||||
if (client.options.attachments) {
|
||||
userMessage.files = client.options.attachments;
|
||||
if (req.body.files && client.options.attachments) {
|
||||
userMessage.files = [];
|
||||
const messageFiles = new Set(req.body.files.map((file) => file.file_id));
|
||||
for (let attachment of client.options.attachments) {
|
||||
if (messageFiles.has(attachment.file_id)) {
|
||||
userMessage.files.push(attachment);
|
||||
}
|
||||
}
|
||||
delete userMessage.image_urls;
|
||||
}
|
||||
|
||||
|
|
@ -109,11 +115,13 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
|||
});
|
||||
res.end();
|
||||
|
||||
await saveMessage(
|
||||
req,
|
||||
{ ...response, user },
|
||||
{ context: 'api/server/controllers/agents/request.js - response end' },
|
||||
);
|
||||
if (!client.savedMessageIds.has(response.messageId)) {
|
||||
await saveMessage(
|
||||
req,
|
||||
{ ...response, user },
|
||||
{ context: 'api/server/controllers/agents/request.js - response end' },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!client.skipSaveUserMessage) {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ const { providerEndpointMap } = require('librechat-data-provider');
|
|||
|
||||
/**
|
||||
* @typedef {import('@librechat/agents').t} t
|
||||
* @typedef {import('@librechat/agents').StandardGraphConfig} StandardGraphConfig
|
||||
* @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
|
||||
|
|
@ -17,18 +17,16 @@ const { providerEndpointMap } = require('librechat-data-provider');
|
|||
* @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 {AbortSignal} options.signal - The signal for this 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,
|
||||
modelOptions,
|
||||
signal,
|
||||
customHandlers,
|
||||
streaming = true,
|
||||
streamUsage = true,
|
||||
|
|
@ -40,14 +38,17 @@ async function createRun({
|
|||
streaming,
|
||||
streamUsage,
|
||||
},
|
||||
modelOptions,
|
||||
agent.model_parameters,
|
||||
);
|
||||
|
||||
/** @type {StandardGraphConfig} */
|
||||
const graphConfig = {
|
||||
tools,
|
||||
signal,
|
||||
llmConfig,
|
||||
tools: agent.tools,
|
||||
instructions: agent.instructions,
|
||||
additional_instructions: agent.additional_instructions,
|
||||
// toolEnd: agent.end_after_tools,
|
||||
};
|
||||
|
||||
// TEMPORARY FOR TESTING
|
||||
|
|
|
|||
|
|
@ -111,7 +111,6 @@ const getAgentHandler = async (req, res) => {
|
|||
isCollaborative: agent.isCollaborative,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json(agent);
|
||||
} catch (error) {
|
||||
logger.error('[/Agents/:id] Error retrieving agent', error);
|
||||
|
|
@ -132,16 +131,24 @@ const updateAgentHandler = async (req, res) => {
|
|||
try {
|
||||
const id = req.params.id;
|
||||
const { projectIds, removeProjectIds, ...updateData } = req.body;
|
||||
const isAdmin = req.user.role === SystemRoles.ADMIN;
|
||||
const existingAgent = await getAgent({ id });
|
||||
const isAuthor = existingAgent.author.toString() === req.user.id;
|
||||
|
||||
let updatedAgent;
|
||||
const query = { id, author: req.user.id };
|
||||
if (req.user.role === SystemRoles.ADMIN) {
|
||||
delete query.author;
|
||||
if (!existingAgent) {
|
||||
return res.status(404).json({ error: 'Agent not found' });
|
||||
}
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
updatedAgent = await updateAgent(query, updateData);
|
||||
const hasEditPermission = existingAgent.isCollaborative || isAdmin || isAuthor;
|
||||
|
||||
if (!hasEditPermission) {
|
||||
return res.status(403).json({
|
||||
error: 'You do not have permission to modify this non-collaborative agent',
|
||||
});
|
||||
}
|
||||
|
||||
let updatedAgent =
|
||||
Object.keys(updateData).length > 0 ? await updateAgent({ id }, updateData) : existingAgent;
|
||||
|
||||
if (projectIds || removeProjectIds) {
|
||||
updatedAgent = await updateAgentProjects({
|
||||
user: req.user,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const { v4 } = require('uuid');
|
||||
const {
|
||||
Time,
|
||||
Constants,
|
||||
RunStatus,
|
||||
CacheKeys,
|
||||
|
|
@ -24,6 +25,7 @@ const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
|
|||
const { formatMessage, createVisionPrompt } = require('~/app/clients/prompts');
|
||||
const { createRun, StreamRunManager } = require('~/server/services/Runs');
|
||||
const { addTitle } = require('~/server/services/Endpoints/assistants');
|
||||
const { createRunBody } = require('~/server/services/createRunBody');
|
||||
const { getTransactions } = require('~/models/Transaction');
|
||||
const checkBalance = require('~/models/checkBalance');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
|
|
@ -32,8 +34,6 @@ const { getModelMaxTokens } = require('~/utils');
|
|||
const { getOpenAIClient } = require('./helpers');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const ten_minutes = 1000 * 60 * 10;
|
||||
|
||||
/**
|
||||
* @route POST /
|
||||
* @desc Chat with an assistant
|
||||
|
|
@ -59,6 +59,7 @@ const chatV1 = async (req, res) => {
|
|||
messageId: _messageId,
|
||||
conversationId: convoId,
|
||||
parentMessageId: _parentId = Constants.NO_PARENT,
|
||||
clientTimestamp,
|
||||
} = req.body;
|
||||
|
||||
/** @type {OpenAIClient} */
|
||||
|
|
@ -304,24 +305,14 @@ const chatV1 = async (req, res) => {
|
|||
};
|
||||
|
||||
/** @type {CreateRunBody | undefined} */
|
||||
const body = {
|
||||
const body = createRunBody({
|
||||
assistant_id,
|
||||
model,
|
||||
};
|
||||
|
||||
if (promptPrefix) {
|
||||
body.additional_instructions = promptPrefix;
|
||||
}
|
||||
|
||||
if (typeof endpointOption.artifactsPrompt === 'string' && endpointOption.artifactsPrompt) {
|
||||
body.additional_instructions = `${body.additional_instructions ?? ''}\n${
|
||||
endpointOption.artifactsPrompt
|
||||
}`.trim();
|
||||
}
|
||||
|
||||
if (instructions) {
|
||||
body.instructions = instructions;
|
||||
}
|
||||
promptPrefix,
|
||||
instructions,
|
||||
endpointOption,
|
||||
clientTimestamp,
|
||||
});
|
||||
|
||||
const getRequestFileIds = async () => {
|
||||
let thread_file_ids = [];
|
||||
|
|
@ -518,7 +509,7 @@ const chatV1 = async (req, res) => {
|
|||
});
|
||||
|
||||
run_id = run.id;
|
||||
await cache.set(cacheKey, `${thread_id}:${run_id}`, ten_minutes);
|
||||
await cache.set(cacheKey, `${thread_id}:${run_id}`, Time.TEN_MINUTES);
|
||||
sendInitialResponse();
|
||||
|
||||
// todo: retry logic
|
||||
|
|
@ -529,7 +520,7 @@ const chatV1 = async (req, res) => {
|
|||
/** @type {{[AssistantStreamEvents.ThreadRunCreated]: (event: ThreadRunCreated) => Promise<void>}} */
|
||||
const handlers = {
|
||||
[AssistantStreamEvents.ThreadRunCreated]: async (event) => {
|
||||
await cache.set(cacheKey, `${thread_id}:${event.data.id}`, ten_minutes);
|
||||
await cache.set(cacheKey, `${thread_id}:${event.data.id}`, Time.TEN_MINUTES);
|
||||
run_id = event.data.id;
|
||||
sendInitialResponse();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const { createErrorHandler } = require('~/server/controllers/assistants/errors')
|
|||
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
|
||||
const { createRun, StreamRunManager } = require('~/server/services/Runs');
|
||||
const { addTitle } = require('~/server/services/Endpoints/assistants');
|
||||
const { createRunBody } = require('~/server/services/createRunBody');
|
||||
const { getTransactions } = require('~/models/Transaction');
|
||||
const checkBalance = require('~/models/checkBalance');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
|
|
@ -31,8 +32,6 @@ const { getModelMaxTokens } = require('~/utils');
|
|||
const { getOpenAIClient } = require('./helpers');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const ten_minutes = 1000 * 60 * 10;
|
||||
|
||||
/**
|
||||
* @route POST /
|
||||
* @desc Chat with an assistant
|
||||
|
|
@ -58,6 +57,7 @@ const chatV2 = async (req, res) => {
|
|||
messageId: _messageId,
|
||||
conversationId: convoId,
|
||||
parentMessageId: _parentId = Constants.NO_PARENT,
|
||||
clientTimestamp,
|
||||
} = req.body;
|
||||
|
||||
/** @type {OpenAIClient} */
|
||||
|
|
@ -186,22 +186,14 @@ const chatV2 = async (req, res) => {
|
|||
};
|
||||
|
||||
/** @type {CreateRunBody | undefined} */
|
||||
const body = {
|
||||
const body = createRunBody({
|
||||
assistant_id,
|
||||
model,
|
||||
};
|
||||
|
||||
if (promptPrefix) {
|
||||
body.additional_instructions = promptPrefix;
|
||||
}
|
||||
|
||||
if (typeof endpointOption.artifactsPrompt === 'string' && endpointOption.artifactsPrompt) {
|
||||
body.additional_instructions = `${body.additional_instructions ?? ''}\n${endpointOption.artifactsPrompt}`.trim();
|
||||
}
|
||||
|
||||
if (instructions) {
|
||||
body.instructions = instructions;
|
||||
}
|
||||
promptPrefix,
|
||||
instructions,
|
||||
endpointOption,
|
||||
clientTimestamp,
|
||||
});
|
||||
|
||||
const getRequestFileIds = async () => {
|
||||
let thread_file_ids = [];
|
||||
|
|
@ -361,7 +353,7 @@ const chatV2 = async (req, res) => {
|
|||
});
|
||||
|
||||
run_id = run.id;
|
||||
await cache.set(cacheKey, `${thread_id}:${run_id}`, ten_minutes);
|
||||
await cache.set(cacheKey, `${thread_id}:${run_id}`, Time.TEN_MINUTES);
|
||||
sendInitialResponse();
|
||||
|
||||
// todo: retry logic
|
||||
|
|
@ -372,7 +364,7 @@ const chatV2 = async (req, res) => {
|
|||
/** @type {{[AssistantStreamEvents.ThreadRunCreated]: (event: ThreadRunCreated) => Promise<void>}} */
|
||||
const handlers = {
|
||||
[AssistantStreamEvents.ThreadRunCreated]: async (event) => {
|
||||
await cache.set(cacheKey, `${thread_id}:${event.data.id}`, ten_minutes);
|
||||
await cache.set(cacheKey, `${thread_id}:${event.data.id}`, Time.TEN_MINUTES);
|
||||
run_id = event.data.id;
|
||||
sendInitialResponse();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,8 +19,15 @@ const createAssistant = async (req, res) => {
|
|||
try {
|
||||
const { openai } = await getOpenAIClient({ req, res });
|
||||
|
||||
const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body;
|
||||
const {
|
||||
tools = [],
|
||||
endpoint,
|
||||
conversation_starters,
|
||||
append_current_datetime,
|
||||
...assistantData
|
||||
} = req.body;
|
||||
delete assistantData.conversation_starters;
|
||||
delete assistantData.append_current_datetime;
|
||||
|
||||
assistantData.tools = tools
|
||||
.map((tool) => {
|
||||
|
|
@ -49,6 +56,9 @@ const createAssistant = async (req, res) => {
|
|||
if (conversation_starters) {
|
||||
createData.conversation_starters = conversation_starters;
|
||||
}
|
||||
if (append_current_datetime !== undefined) {
|
||||
createData.append_current_datetime = append_current_datetime;
|
||||
}
|
||||
|
||||
const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData);
|
||||
|
||||
|
|
@ -60,6 +70,10 @@ const createAssistant = async (req, res) => {
|
|||
assistant.conversation_starters = document.conversation_starters;
|
||||
}
|
||||
|
||||
if (append_current_datetime !== undefined) {
|
||||
assistant.append_current_datetime = append_current_datetime;
|
||||
}
|
||||
|
||||
logger.debug('/assistants/', assistant);
|
||||
res.status(201).json(assistant);
|
||||
} catch (error) {
|
||||
|
|
@ -102,7 +116,12 @@ const patchAssistant = async (req, res) => {
|
|||
await validateAuthor({ req, openai });
|
||||
|
||||
const assistant_id = req.params.id;
|
||||
const { endpoint: _e, conversation_starters, ...updateData } = req.body;
|
||||
const {
|
||||
endpoint: _e,
|
||||
conversation_starters,
|
||||
append_current_datetime,
|
||||
...updateData
|
||||
} = req.body;
|
||||
updateData.tools = (updateData.tools ?? [])
|
||||
.map((tool) => {
|
||||
if (typeof tool !== 'string') {
|
||||
|
|
@ -127,6 +146,11 @@ const patchAssistant = async (req, res) => {
|
|||
updatedAssistant.conversation_starters = conversationStartersUpdate.conversation_starters;
|
||||
}
|
||||
|
||||
if (append_current_datetime !== undefined) {
|
||||
await updateAssistantDoc({ assistant_id }, { append_current_datetime });
|
||||
updatedAssistant.append_current_datetime = append_current_datetime;
|
||||
}
|
||||
|
||||
res.json(updatedAssistant);
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/:id] Error updating assistant', error);
|
||||
|
|
@ -219,6 +243,7 @@ const getAssistantDocuments = async (req, res) => {
|
|||
conversation_starters: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
append_current_datetime: 1,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,15 @@ const createAssistant = async (req, res) => {
|
|||
/** @type {{ openai: OpenAIClient }} */
|
||||
const { openai } = await getOpenAIClient({ req, res });
|
||||
|
||||
const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body;
|
||||
const {
|
||||
tools = [],
|
||||
endpoint,
|
||||
conversation_starters,
|
||||
append_current_datetime,
|
||||
...assistantData
|
||||
} = req.body;
|
||||
delete assistantData.conversation_starters;
|
||||
delete assistantData.append_current_datetime;
|
||||
|
||||
assistantData.tools = tools
|
||||
.map((tool) => {
|
||||
|
|
@ -46,6 +53,9 @@ const createAssistant = async (req, res) => {
|
|||
if (conversation_starters) {
|
||||
createData.conversation_starters = conversation_starters;
|
||||
}
|
||||
if (append_current_datetime !== undefined) {
|
||||
createData.append_current_datetime = append_current_datetime;
|
||||
}
|
||||
|
||||
const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData);
|
||||
|
||||
|
|
@ -56,6 +66,9 @@ const createAssistant = async (req, res) => {
|
|||
if (document.conversation_starters) {
|
||||
assistant.conversation_starters = document.conversation_starters;
|
||||
}
|
||||
if (append_current_datetime !== undefined) {
|
||||
assistant.append_current_datetime = append_current_datetime;
|
||||
}
|
||||
|
||||
logger.debug('/assistants/', assistant);
|
||||
res.status(201).json(assistant);
|
||||
|
|
@ -89,6 +102,14 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
|
|||
delete updateData.conversation_starters;
|
||||
}
|
||||
|
||||
if (updateData?.append_current_datetime !== undefined) {
|
||||
await updateAssistantDoc(
|
||||
{ assistant_id: assistant_id },
|
||||
{ append_current_datetime: updateData.append_current_datetime },
|
||||
);
|
||||
delete updateData.append_current_datetime;
|
||||
}
|
||||
|
||||
let hasFileSearch = false;
|
||||
for (const tool of updateData.tools ?? []) {
|
||||
let actualTool = typeof tool === 'string' ? req.app.locals.availableTools[tool] : tool;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
const { nanoid } = require('nanoid');
|
||||
const { EnvVar } = require('@librechat/agents');
|
||||
const { Tools, AuthType } = require('librechat-data-provider');
|
||||
const { loadAuthValues } = require('~/app/clients/tools/util');
|
||||
const { Tools, AuthType, ToolCallTypes } = require('librechat-data-provider');
|
||||
const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process');
|
||||
const { processCodeOutput } = require('~/server/services/Files/Code/process');
|
||||
const { loadAuthValues, loadTools } = require('~/app/clients/tools/util');
|
||||
const { createToolCall, getToolCallsByConvo } = require('~/models/ToolCall');
|
||||
const { getMessage } = require('~/models/Message');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const fieldsMap = {
|
||||
[Tools.execute_code]: [EnvVar.CODE_API_KEY],
|
||||
|
|
@ -24,6 +30,7 @@ const verifyToolAuth = async (req, res) => {
|
|||
result = await loadAuthValues({
|
||||
userId: req.user.id,
|
||||
authFields,
|
||||
throwError: false,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(200).json({ authenticated: false, message: AuthType.USER_PROVIDED });
|
||||
|
|
@ -48,6 +55,131 @@ const verifyToolAuth = async (req, res) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {ServerRequest} req - The request object, containing information about the HTTP request.
|
||||
* @param {ServerResponse} res - The response object, used to send back the desired HTTP response.
|
||||
* @returns {Promise<void>} A promise that resolves when the function has completed.
|
||||
*/
|
||||
const callTool = async (req, res) => {
|
||||
try {
|
||||
const { toolId = '' } = req.params;
|
||||
if (!fieldsMap[toolId]) {
|
||||
logger.warn(`[${toolId}/call] User ${req.user.id} attempted call to invalid tool`);
|
||||
res.status(404).json({ message: 'Tool not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { partIndex, blockIndex, messageId, conversationId, ...args } = req.body;
|
||||
if (!messageId) {
|
||||
logger.warn(`[${toolId}/call] User ${req.user.id} attempted call without message ID`);
|
||||
res.status(400).json({ message: 'Message ID required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await getMessage({ user: req.user.id, messageId });
|
||||
if (!message) {
|
||||
logger.debug(`[${toolId}/call] User ${req.user.id} attempted call with invalid message ID`);
|
||||
res.status(404).json({ message: 'Message not found' });
|
||||
return;
|
||||
}
|
||||
logger.debug(`[${toolId}/call] User: ${req.user.id}`);
|
||||
const { loadedTools } = await loadTools({
|
||||
user: req.user.id,
|
||||
tools: [toolId],
|
||||
functions: true,
|
||||
options: {
|
||||
req,
|
||||
returnMetadata: true,
|
||||
processFileURL,
|
||||
uploadImageBuffer,
|
||||
fileStrategy: req.app.locals.fileStrategy,
|
||||
},
|
||||
});
|
||||
|
||||
const tool = loadedTools[0];
|
||||
const toolCallId = `${req.user.id}_${nanoid()}`;
|
||||
const result = await tool.invoke({
|
||||
args,
|
||||
name: toolId,
|
||||
id: toolCallId,
|
||||
type: ToolCallTypes.TOOL_CALL,
|
||||
});
|
||||
|
||||
const { content, artifact } = result;
|
||||
const toolCallData = {
|
||||
toolId,
|
||||
messageId,
|
||||
partIndex,
|
||||
blockIndex,
|
||||
conversationId,
|
||||
result: content,
|
||||
user: req.user.id,
|
||||
};
|
||||
|
||||
if (!artifact || !artifact.files || toolId !== Tools.execute_code) {
|
||||
createToolCall(toolCallData).catch((error) => {
|
||||
logger.error(`Error creating tool call: ${error.message}`);
|
||||
});
|
||||
return res.status(200).json({
|
||||
result: content,
|
||||
});
|
||||
}
|
||||
|
||||
const artifactPromises = [];
|
||||
for (const file of artifact.files) {
|
||||
const { id, name } = file;
|
||||
artifactPromises.push(
|
||||
(async () => {
|
||||
const fileMetadata = await processCodeOutput({
|
||||
req,
|
||||
id,
|
||||
name,
|
||||
apiKey: tool.apiKey,
|
||||
messageId,
|
||||
toolCallId,
|
||||
conversationId,
|
||||
session_id: artifact.session_id,
|
||||
});
|
||||
|
||||
if (!fileMetadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fileMetadata;
|
||||
})().catch((error) => {
|
||||
logger.error('Error processing code output:', error);
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
}
|
||||
const attachments = await Promise.all(artifactPromises);
|
||||
toolCallData.attachments = attachments;
|
||||
createToolCall(toolCallData).catch((error) => {
|
||||
logger.error(`Error creating tool call: ${error.message}`);
|
||||
});
|
||||
res.status(200).json({
|
||||
result: content,
|
||||
attachments,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error calling tool', error);
|
||||
res.status(500).json({ message: 'Error calling tool' });
|
||||
}
|
||||
};
|
||||
|
||||
const getToolCalls = async (req, res) => {
|
||||
try {
|
||||
const { conversationId } = req.query;
|
||||
const toolCalls = await getToolCallsByConvo(conversationId, req.user.id);
|
||||
res.status(200).json(toolCalls);
|
||||
} catch (error) {
|
||||
logger.error('Error getting tool calls', error);
|
||||
res.status(500).json({ message: 'Error getting tool calls' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
callTool,
|
||||
getToolCalls,
|
||||
verifyToolAuth,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ async function abortRun(req, res) {
|
|||
const cacheKey = `${req.user.id}:${conversationId}`;
|
||||
const cache = getLogStores(CacheKeys.ABORT_KEYS);
|
||||
const runValues = await cache.get(cacheKey);
|
||||
if (!runValues) {
|
||||
logger.warn('[abortRun] Run not found in cache', { cacheKey });
|
||||
return res.status(204).send({ message: 'Run not found' });
|
||||
}
|
||||
const [thread_id, run_id] = runValues.split(':');
|
||||
|
||||
if (!run_id) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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 { getConvoFiles } = require('~/models/Conversation');
|
||||
const { handleError } = require('~/server/utils');
|
||||
|
||||
const buildFunction = {
|
||||
|
|
@ -62,6 +63,10 @@ async function buildEndpointOption(req, res, next) {
|
|||
}
|
||||
|
||||
try {
|
||||
currentModelSpec.preset.spec = spec;
|
||||
if (currentModelSpec.iconURL != null && currentModelSpec.iconURL !== '') {
|
||||
currentModelSpec.preset.iconURL = currentModelSpec.iconURL;
|
||||
}
|
||||
parsedBody = parseCompactConvo({
|
||||
endpoint,
|
||||
endpointType,
|
||||
|
|
@ -72,21 +77,32 @@ async function buildEndpointOption(req, res, next) {
|
|||
}
|
||||
}
|
||||
|
||||
const endpointFn = buildFunction[endpointType ?? endpoint];
|
||||
const builder = isAgentsEndpoint(endpoint) ? (...args) => endpointFn(req, ...args) : endpointFn;
|
||||
try {
|
||||
const isAgents = isAgentsEndpoint(endpoint);
|
||||
const endpointFn = buildFunction[endpointType ?? endpoint];
|
||||
const builder = isAgents ? (...args) => endpointFn(req, ...args) : endpointFn;
|
||||
|
||||
// TODO: use object params
|
||||
req.body.endpointOption = builder(endpoint, parsedBody, endpointType);
|
||||
// TODO: use object params
|
||||
req.body.endpointOption = await builder(endpoint, parsedBody, endpointType);
|
||||
|
||||
// TODO: use `getModelsConfig` only when necessary
|
||||
const modelsConfig = await getModelsConfig(req);
|
||||
req.body.endpointOption.modelsConfig = modelsConfig;
|
||||
|
||||
if (req.body.files) {
|
||||
// hold the promise
|
||||
req.body.endpointOption.attachments = processFiles(req.body.files);
|
||||
// TODO: use `getModelsConfig` only when necessary
|
||||
const modelsConfig = await getModelsConfig(req);
|
||||
const { resendFiles = true } = req.body.endpointOption;
|
||||
req.body.endpointOption.modelsConfig = modelsConfig;
|
||||
if (isAgents && resendFiles && req.body.conversationId) {
|
||||
const fileIds = await getConvoFiles(req.body.conversationId);
|
||||
const requestFiles = req.body.files ?? [];
|
||||
if (requestFiles.length || fileIds.length) {
|
||||
req.body.endpointOption.attachments = processFiles(requestFiles, fileIds);
|
||||
}
|
||||
} else if (req.body.files) {
|
||||
// hold the promise
|
||||
req.body.endpointOption.attachments = processFiles(req.body.files);
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
return handleError(res, { text: 'Error building endpoint option' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = buildEndpointOption;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const { isDomainAllowed } = require('~/server/services/AuthService');
|
||||
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
|
|
@ -14,7 +14,7 @@ const { logger } = require('~/config');
|
|||
*/
|
||||
const checkDomainAllowed = async (req, res, next = () => {}) => {
|
||||
const email = req?.user?.email;
|
||||
if (email && !(await isDomainAllowed(email))) {
|
||||
if (email && !(await isEmailDomainAllowed(email))) {
|
||||
logger.error(`[Social Login] [Social Login not allowed] [Email: ${email}]`);
|
||||
return res.redirect('/login');
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const loginLimiter = require('./loginLimiter');
|
|||
const importLimiters = require('./importLimiters');
|
||||
const uploadLimiters = require('./uploadLimiters');
|
||||
const registerLimiter = require('./registerLimiter');
|
||||
const toolCallLimiter = require('./toolCallLimiter');
|
||||
const messageLimiters = require('./messageLimiters');
|
||||
const verifyEmailLimiter = require('./verifyEmailLimiter');
|
||||
const resetPasswordLimiter = require('./resetPasswordLimiter');
|
||||
|
|
@ -15,6 +16,7 @@ module.exports = {
|
|||
...messageLimiters,
|
||||
loginLimiter,
|
||||
registerLimiter,
|
||||
toolCallLimiter,
|
||||
createTTSLimiters,
|
||||
createSTTLimiters,
|
||||
verifyEmailLimiter,
|
||||
|
|
|
|||
25
api/server/middleware/limiters/toolCallLimiter.js
Normal file
25
api/server/middleware/limiters/toolCallLimiter.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const logViolation = require('~/cache/logViolation');
|
||||
|
||||
const toolCallLimiter = rateLimit({
|
||||
windowMs: 1000,
|
||||
max: 1,
|
||||
handler: async (req, res) => {
|
||||
const type = ViolationTypes.TOOL_CALL_LIMIT;
|
||||
const errorMessage = {
|
||||
type,
|
||||
max: 1,
|
||||
limiter: 'user',
|
||||
windowInMinutes: 1,
|
||||
};
|
||||
|
||||
await logViolation(req, res, type, errorMessage, 0);
|
||||
res.status(429).json({ message: 'Too many tool call requests. Try again later' });
|
||||
},
|
||||
keyGenerator: function (req) {
|
||||
return req.user?.id;
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = toolCallLimiter;
|
||||
|
|
@ -3,6 +3,7 @@ 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 { isActionDomainAllowed } = require('~/server/services/domains');
|
||||
const { getAgent, updateAgent } = require('~/models/Agent');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
|
|
@ -42,6 +43,10 @@ router.post('/:agent_id', async (req, res) => {
|
|||
}
|
||||
|
||||
let metadata = await encryptMetadata(_metadata);
|
||||
const isDomainAllowed = await isActionDomainAllowed(metadata.domain);
|
||||
if (!isDomainAllowed) {
|
||||
return res.status(400).json({ message: 'Domain not allowed' });
|
||||
}
|
||||
|
||||
let { domain } = metadata;
|
||||
domain = await domainParser(req, domain, true);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
const express = require('express');
|
||||
|
||||
const router = express.Router();
|
||||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||
const {
|
||||
setHeaders,
|
||||
handleAbort,
|
||||
// validateModel,
|
||||
// validateEndpoint,
|
||||
generateCheckAccess,
|
||||
validateConvoAccess,
|
||||
buildEndpointOption,
|
||||
} = require('~/server/middleware');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/agents');
|
||||
const AgentController = require('~/server/controllers/agents/request');
|
||||
const addTitle = require('~/server/services/Endpoints/agents/title');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/abort', handleAbort());
|
||||
|
||||
const checkAgentAccess = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]);
|
||||
|
||||
/**
|
||||
* @route POST /
|
||||
* @desc Chat with an assistant
|
||||
|
|
@ -25,7 +29,8 @@ router.post('/abort', handleAbort());
|
|||
router.post(
|
||||
'/',
|
||||
// validateModel,
|
||||
// validateEndpoint,
|
||||
checkAgentAccess,
|
||||
validateConvoAccess,
|
||||
buildEndpointOption,
|
||||
setHeaders,
|
||||
async (req, res, next) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const express = require('express');
|
||||
const { callTool, verifyToolAuth, getToolCalls } = require('~/server/controllers/tools');
|
||||
const { getAvailableTools } = require('~/server/controllers/PluginController');
|
||||
const { verifyToolAuth } = require('~/server/controllers/tools');
|
||||
const { toolCallLimiter } = require('~/server/middleware/limiters');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
|
@ -11,6 +12,13 @@ const router = express.Router();
|
|||
*/
|
||||
router.get('/', getAvailableTools);
|
||||
|
||||
/**
|
||||
* Get a list of tool calls.
|
||||
* @route GET /agents/tools/calls
|
||||
* @returns {ToolCallData[]} 200 - application/json
|
||||
*/
|
||||
router.get('/calls', getToolCalls);
|
||||
|
||||
/**
|
||||
* Verify authentication for a specific tool
|
||||
* @route GET /agents/tools/:toolId/auth
|
||||
|
|
@ -19,4 +27,13 @@ router.get('/', getAvailableTools);
|
|||
*/
|
||||
router.get('/:toolId/auth', verifyToolAuth);
|
||||
|
||||
/**
|
||||
* Execute code for a specific tool
|
||||
* @route POST /agents/tools/:toolId/call
|
||||
* @param {string} toolId - The ID of the tool to execute
|
||||
* @param {object} req.body - Request body
|
||||
* @returns {object} Result of code execution
|
||||
*/
|
||||
router.post('/:toolId/call', toolCallLimiter, callTool);
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
const express = require('express');
|
||||
const { nanoid } = require('nanoid');
|
||||
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
||||
const { actionDelimiter, EModelEndpoint } = require('librechat-data-provider');
|
||||
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
||||
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
||||
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
||||
const { updateAssistantDoc, getAssistant } = require('~/models/Assistant');
|
||||
const { isActionDomainAllowed } = require('~/server/services/domains');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const router = express.Router();
|
||||
|
|
@ -29,6 +30,10 @@ router.post('/:assistant_id', async (req, res) => {
|
|||
}
|
||||
|
||||
let metadata = await encryptMetadata(_metadata);
|
||||
const isDomainAllowed = await isActionDomainAllowed(metadata.domain);
|
||||
if (!isDomainAllowed) {
|
||||
return res.status(400).json({ message: 'Domain not allowed' });
|
||||
}
|
||||
|
||||
let { domain } = metadata;
|
||||
domain = await domainParser(req, domain, true);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
|
|||
const { forkConversation } = require('~/server/utils/import/fork');
|
||||
const { importConversations } = require('~/server/utils/import');
|
||||
const { createImportLimiters } = require('~/server/middleware');
|
||||
const { deleteToolCalls } = require('~/models/ToolCall');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { sleep } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
|
@ -105,6 +106,7 @@ router.post('/clear', async (req, res) => {
|
|||
|
||||
try {
|
||||
const dbResponse = await deleteConvos(req.user.id, filter);
|
||||
await deleteToolCalls(req.user.id, filter.conversationId);
|
||||
res.status(201).json(dbResponse);
|
||||
} catch (error) {
|
||||
logger.error('Error clearing conversations', error);
|
||||
|
|
|
|||
|
|
@ -107,6 +107,10 @@ router.delete('/', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
function isValidID(str) {
|
||||
return /^[A-Za-z0-9_-]{21}$/.test(str);
|
||||
}
|
||||
|
||||
router.get('/code/download/:session_id/:fileId', async (req, res) => {
|
||||
try {
|
||||
const { session_id, fileId } = req.params;
|
||||
|
|
@ -117,6 +121,11 @@ router.get('/code/download/:session_id/:fileId', async (req, res) => {
|
|||
return res.status(400).send('Bad request');
|
||||
}
|
||||
|
||||
if (!isValidID(session_id) || !isValidID(fileId)) {
|
||||
logger.debug(`${logPrefix} invalid session_id or fileId`);
|
||||
return res.status(400).send('Bad request');
|
||||
}
|
||||
|
||||
const { getDownloadStream } = getStrategyFunctions(FileSources.execute_code);
|
||||
if (!getDownloadStream) {
|
||||
logger.warn(
|
||||
|
|
@ -213,21 +222,20 @@ router.get('/download/:userId/:file_id', async (req, res) => {
|
|||
});
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const file = req.file;
|
||||
const metadata = req.body;
|
||||
let cleanup = true;
|
||||
|
||||
try {
|
||||
filterFile({ req, file });
|
||||
filterFile({ req });
|
||||
|
||||
metadata.temp_file_id = metadata.file_id;
|
||||
metadata.file_id = req.file_id;
|
||||
|
||||
if (isAgentsEndpoint(metadata.endpoint)) {
|
||||
return await processAgentFileUpload({ req, res, file, metadata });
|
||||
return await processAgentFileUpload({ req, res, metadata });
|
||||
}
|
||||
|
||||
await processFileUpload({ req, res, file, metadata });
|
||||
await processFileUpload({ req, res, metadata });
|
||||
} catch (error) {
|
||||
let message = 'Error processing file';
|
||||
logger.error('[/files] Error processing file:', error);
|
||||
|
|
@ -238,7 +246,7 @@ router.post('/', async (req, res) => {
|
|||
|
||||
// TODO: delete remote file if it exists
|
||||
try {
|
||||
await fs.unlink(file.path);
|
||||
await fs.unlink(req.file.path);
|
||||
cleanup = false;
|
||||
} catch (error) {
|
||||
logger.error('[/files] Error deleting file:', error);
|
||||
|
|
@ -248,7 +256,7 @@ router.post('/', async (req, res) => {
|
|||
|
||||
if (cleanup) {
|
||||
try {
|
||||
await fs.unlink(file.path);
|
||||
await fs.unlink(req.file.path);
|
||||
} catch (error) {
|
||||
logger.error('[/files] Error deleting file after file processing:', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const express = require('express');
|
||||
const { filterFile, processImageFile } = require('~/server/services/Files/process');
|
||||
const { isAgentsEndpoint } = require('librechat-data-provider');
|
||||
const {
|
||||
filterFile,
|
||||
processImageFile,
|
||||
processAgentFileUpload,
|
||||
} = require('~/server/services/Files/process');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const router = express.Router();
|
||||
|
|
@ -10,12 +15,16 @@ router.post('/', async (req, res) => {
|
|||
const metadata = req.body;
|
||||
|
||||
try {
|
||||
filterFile({ req, file: req.file, image: true });
|
||||
filterFile({ req, image: true });
|
||||
|
||||
metadata.temp_file_id = metadata.file_id;
|
||||
metadata.file_id = req.file_id;
|
||||
|
||||
await processImageFile({ req, res, file: req.file, metadata });
|
||||
if (isAgentsEndpoint(metadata.endpoint) && metadata.tool_resource != null) {
|
||||
return await processAgentFileUpload({ req, res, metadata });
|
||||
}
|
||||
|
||||
await processImageFile({ req, res, metadata });
|
||||
} catch (error) {
|
||||
// TODO: delete remote file if it exists
|
||||
logger.error('[/files/images] Error processing file:', error);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const express = require('express');
|
||||
const {
|
||||
promptPermissionsSchema,
|
||||
agentPermissionsSchema,
|
||||
PermissionTypes,
|
||||
roleDefaults,
|
||||
SystemRoles,
|
||||
|
|
@ -72,4 +73,37 @@ router.put('/:roleName/prompts', checkAdmin, async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/roles/:roleName/agents
|
||||
* Update agent permissions for a specific role
|
||||
*/
|
||||
router.put('/:roleName/agents', checkAdmin, async (req, res) => {
|
||||
const { roleName: _r } = req.params;
|
||||
// TODO: TEMP, use a better parsing for roleName
|
||||
const roleName = _r.toUpperCase();
|
||||
/** @type {TRole['AGENTS']} */
|
||||
const updates = req.body;
|
||||
|
||||
try {
|
||||
const parsedUpdates = agentPermissionsSchema.partial().parse(updates);
|
||||
|
||||
const role = await getRoleByName(roleName);
|
||||
if (!role) {
|
||||
return res.status(404).send({ message: 'Role not found' });
|
||||
}
|
||||
|
||||
const mergedUpdates = {
|
||||
[PermissionTypes.AGENTS]: {
|
||||
...role[PermissionTypes.AGENTS],
|
||||
...parsedUpdates,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedRole = await updateRoleByName(roleName, mergedUpdates);
|
||||
res.status(200).send(updatedRole);
|
||||
} catch (error) {
|
||||
return res.status(400).send({ message: 'Invalid prompt permissions.', error: error.errors });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const {
|
|||
actionDomainSeparator,
|
||||
} = require('librechat-data-provider');
|
||||
const { tool } = require('@langchain/core/tools');
|
||||
const { isActionDomainAllowed } = require('~/server/services/domains');
|
||||
const { encryptV2, decryptV2 } = require('~/server/utils/crypto');
|
||||
const { getActions, deleteActions } = require('~/models/Action');
|
||||
const { deleteAssistant } = require('~/models/Assistant');
|
||||
|
|
@ -122,6 +123,10 @@ async function loadActionSets(searchParams) {
|
|||
*/
|
||||
async function createActionTool({ action, requestBuilder, zodSchema, name, description }) {
|
||||
action.metadata = await decryptMetadata(action.metadata);
|
||||
const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain);
|
||||
if (!isDomainAllowed) {
|
||||
return null;
|
||||
}
|
||||
/** @type {(toolInput: Object | string) => Promise<unknown>} */
|
||||
const _call = async (toolInput) => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ const { Constants, EModelEndpoint, actionDomainSeparator } = require('librechat-
|
|||
const { domainParser } = require('./ActionService');
|
||||
|
||||
jest.mock('keyv');
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
getCustomConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
const globalCache = {};
|
||||
jest.mock('~/cache/getLogStores', () => {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ const handleRateLimits = require('./Config/handleRateLimits');
|
|||
const { loadDefaultInterface } = require('./start/interface');
|
||||
const { azureConfigSetup } = require('./start/azureOpenAI');
|
||||
const { loadAndFormatTools } = require('./ToolService');
|
||||
const { agentsConfigSetup } = require('./start/agents');
|
||||
const { initializeRoles } = require('~/models/Role');
|
||||
const { cleanup } = require('./cleanup');
|
||||
const paths = require('~/config/paths');
|
||||
|
||||
/**
|
||||
|
|
@ -18,7 +18,6 @@ const paths = require('~/config/paths');
|
|||
* @param {Express.Application} app - The Express application object.
|
||||
*/
|
||||
const AppService = async (app) => {
|
||||
cleanup();
|
||||
await initializeRoles();
|
||||
/** @type {TCustomConfig}*/
|
||||
const config = (await loadCustomConfig()) ?? {};
|
||||
|
|
@ -96,6 +95,10 @@ const AppService = async (app) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (endpoints?.[EModelEndpoint.agents]) {
|
||||
endpointLocals[EModelEndpoint.agents] = agentsConfigSetup(config);
|
||||
}
|
||||
|
||||
const endpointKeys = [
|
||||
EModelEndpoint.openAI,
|
||||
EModelEndpoint.google,
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ const {
|
|||
} = require('~/models/userMethods');
|
||||
const { createToken, findToken, deleteTokens, Session } = require('~/models');
|
||||
const { isEnabled, checkEmailConfig, sendEmail } = require('~/server/utils');
|
||||
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
||||
const { registerSchema } = require('~/strategies/validators');
|
||||
const { hashToken } = require('~/server/utils/crypto');
|
||||
const isDomainAllowed = require('./isDomainAllowed');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const domains = {
|
||||
|
|
@ -165,7 +165,7 @@ const registerUser = async (user, additionalData = {}) => {
|
|||
return { status: 200, message: genericVerificationMessage };
|
||||
}
|
||||
|
||||
if (!(await isDomainAllowed(email))) {
|
||||
if (!(await isEmailDomainAllowed(email))) {
|
||||
const errorMessage =
|
||||
'The email address provided cannot be used. Please use a different email address.';
|
||||
logger.error(`[registerUser] [Registration not allowed] [Email: ${user.email}]`);
|
||||
|
|
@ -422,7 +422,6 @@ module.exports = {
|
|||
registerUser,
|
||||
setAuthTokens,
|
||||
resetPassword,
|
||||
isDomainAllowed,
|
||||
requestPasswordReset,
|
||||
resendVerificationEmail,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -49,10 +49,6 @@ module.exports = {
|
|||
process.env.BEDROCK_AWS_SECRET_ACCESS_KEY ?? process.env.BEDROCK_AWS_DEFAULT_REGION,
|
||||
),
|
||||
/* key will be part of separate config */
|
||||
[EModelEndpoint.agents]: generateConfig(
|
||||
process.env.EXPERIMENTAL_AGENTS,
|
||||
undefined,
|
||||
EModelEndpoint.agents,
|
||||
),
|
||||
[EModelEndpoint.agents]: generateConfig('true', undefined, EModelEndpoint.agents),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,8 +2,14 @@ const { loadAgent } = require('~/models/Agent');
|
|||
const { logger } = require('~/config');
|
||||
|
||||
const buildOptions = (req, endpoint, parsedBody) => {
|
||||
const { agent_id, instructions, spec, ...model_parameters } = parsedBody;
|
||||
|
||||
const {
|
||||
agent_id,
|
||||
instructions,
|
||||
spec,
|
||||
maxContextTokens,
|
||||
resendFiles = true,
|
||||
...model_parameters
|
||||
} = parsedBody;
|
||||
const agentPromise = loadAgent({
|
||||
req,
|
||||
agent_id,
|
||||
|
|
@ -13,12 +19,14 @@ const buildOptions = (req, endpoint, parsedBody) => {
|
|||
});
|
||||
|
||||
const endpointOption = {
|
||||
agent: agentPromise,
|
||||
spec,
|
||||
endpoint,
|
||||
agent_id,
|
||||
resendFiles,
|
||||
instructions,
|
||||
spec,
|
||||
maxContextTokens,
|
||||
model_parameters,
|
||||
agent: agentPromise,
|
||||
};
|
||||
|
||||
return endpointOption;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ const { getCustomEndpointConfig } = require('~/server/services/Config');
|
|||
const { loadAgentTools } = require('~/server/services/ToolService');
|
||||
const AgentClient = require('~/server/controllers/agents/client');
|
||||
const { getModelMaxTokens } = require('~/utils');
|
||||
const { getAgent } = require('~/models/Agent');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const providerConfigMap = {
|
||||
[EModelEndpoint.openAI]: initOpenAI,
|
||||
|
|
@ -25,6 +27,113 @@ const providerConfigMap = {
|
|||
[Providers.OLLAMA]: initCustom,
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Promise<Array<MongoFile | null>> | undefined} _attachments
|
||||
* @param {AgentToolResources | undefined} _tool_resources
|
||||
* @returns {Promise<{ attachments: Array<MongoFile | undefined> | undefined, tool_resources: AgentToolResources | undefined }>}
|
||||
*/
|
||||
const primeResources = async (_attachments, _tool_resources) => {
|
||||
try {
|
||||
if (!_attachments) {
|
||||
return { attachments: undefined, tool_resources: _tool_resources };
|
||||
}
|
||||
/** @type {Array<MongoFile | undefined> | undefined} */
|
||||
const files = await _attachments;
|
||||
const attachments = [];
|
||||
const tool_resources = _tool_resources ?? {};
|
||||
|
||||
for (const file of files) {
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
if (file.metadata?.fileIdentifier) {
|
||||
const execute_code = tool_resources.execute_code ?? {};
|
||||
if (!execute_code.files) {
|
||||
tool_resources.execute_code = { ...execute_code, files: [] };
|
||||
}
|
||||
tool_resources.execute_code.files.push(file);
|
||||
} else if (file.embedded === true) {
|
||||
const file_search = tool_resources.file_search ?? {};
|
||||
if (!file_search.files) {
|
||||
tool_resources.file_search = { ...file_search, files: [] };
|
||||
}
|
||||
tool_resources.file_search.files.push(file);
|
||||
}
|
||||
|
||||
attachments.push(file);
|
||||
}
|
||||
return { attachments, tool_resources };
|
||||
} catch (error) {
|
||||
logger.error('Error priming resources', error);
|
||||
return { attachments: _attachments, tool_resources: _tool_resources };
|
||||
}
|
||||
};
|
||||
|
||||
const initializeAgentOptions = async ({
|
||||
req,
|
||||
res,
|
||||
agent,
|
||||
endpointOption,
|
||||
tool_resources,
|
||||
isInitialAgent = false,
|
||||
}) => {
|
||||
const { tools, toolContextMap } = await loadAgentTools({
|
||||
req,
|
||||
tools: agent.tools,
|
||||
agent_id: agent.id,
|
||||
tool_resources,
|
||||
});
|
||||
|
||||
const provider = agent.provider;
|
||||
let getOptions = providerConfigMap[provider];
|
||||
|
||||
if (!getOptions) {
|
||||
const customEndpointConfig = await getCustomEndpointConfig(provider);
|
||||
if (!customEndpointConfig) {
|
||||
throw new Error(`Provider ${provider} not supported`);
|
||||
}
|
||||
getOptions = initCustom;
|
||||
agent.provider = Providers.OPENAI;
|
||||
agent.endpoint = provider.toLowerCase();
|
||||
}
|
||||
|
||||
const model_parameters = agent.model_parameters ?? { model: agent.model };
|
||||
const _endpointOption = isInitialAgent
|
||||
? endpointOption
|
||||
: {
|
||||
model_parameters,
|
||||
};
|
||||
|
||||
const options = await getOptions({
|
||||
req,
|
||||
res,
|
||||
optionsOnly: true,
|
||||
overrideEndpoint: provider,
|
||||
overrideModel: agent.model,
|
||||
endpointOption: _endpointOption,
|
||||
});
|
||||
|
||||
agent.model_parameters = Object.assign(model_parameters, options.llmConfig);
|
||||
if (options.configOptions) {
|
||||
agent.model_parameters.configuration = options.configOptions;
|
||||
}
|
||||
|
||||
if (!agent.model_parameters.model) {
|
||||
agent.model_parameters.model = agent.model;
|
||||
}
|
||||
|
||||
return {
|
||||
...agent,
|
||||
tools,
|
||||
toolContextMap,
|
||||
maxContextTokens:
|
||||
agent.max_context_tokens ??
|
||||
getModelMaxTokens(agent.model_parameters.model, providerEndpointMap[provider]) ??
|
||||
4000,
|
||||
};
|
||||
};
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
if (!endpointOption) {
|
||||
throw new Error('Endpoint option not provided');
|
||||
|
|
@ -48,70 +157,68 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
throw new Error('No agent promise provided');
|
||||
}
|
||||
|
||||
/** @type {Agent | null} */
|
||||
const agent = await endpointOption.agent;
|
||||
if (!agent) {
|
||||
// Initialize primary agent
|
||||
const primaryAgent = await endpointOption.agent;
|
||||
if (!primaryAgent) {
|
||||
throw new Error('Agent not found');
|
||||
}
|
||||
|
||||
const { tools } = await loadAgentTools({
|
||||
req,
|
||||
tools: agent.tools,
|
||||
agent_id: agent.id,
|
||||
tool_resources: agent.tool_resources,
|
||||
});
|
||||
const { attachments, tool_resources } = await primeResources(
|
||||
endpointOption.attachments,
|
||||
primaryAgent.tool_resources,
|
||||
);
|
||||
|
||||
const provider = agent.provider;
|
||||
let modelOptions = { model: agent.model };
|
||||
let getOptions = providerConfigMap[provider];
|
||||
if (!getOptions) {
|
||||
const customEndpointConfig = await getCustomEndpointConfig(provider);
|
||||
if (!customEndpointConfig) {
|
||||
throw new Error(`Provider ${provider} not supported`);
|
||||
}
|
||||
getOptions = initCustom;
|
||||
agent.provider = Providers.OPENAI;
|
||||
agent.endpoint = provider.toLowerCase();
|
||||
}
|
||||
const agentConfigs = new Map();
|
||||
|
||||
// TODO: pass-in override settings that are specific to current run
|
||||
endpointOption.model_parameters.model = agent.model;
|
||||
const options = await getOptions({
|
||||
// Handle primary agent
|
||||
const primaryConfig = await initializeAgentOptions({
|
||||
req,
|
||||
res,
|
||||
agent: primaryAgent,
|
||||
endpointOption,
|
||||
optionsOnly: true,
|
||||
overrideEndpoint: provider,
|
||||
overrideModel: agent.model,
|
||||
tool_resources,
|
||||
isInitialAgent: true,
|
||||
});
|
||||
|
||||
modelOptions = Object.assign(modelOptions, options.llmConfig);
|
||||
if (options.configOptions) {
|
||||
modelOptions.configuration = options.configOptions;
|
||||
const agent_ids = primaryConfig.agent_ids;
|
||||
if (agent_ids?.length) {
|
||||
for (const agentId of agent_ids) {
|
||||
const agent = await getAgent({ id: agentId });
|
||||
if (!agent) {
|
||||
throw new Error(`Agent ${agentId} not found`);
|
||||
}
|
||||
const config = await initializeAgentOptions({
|
||||
req,
|
||||
res,
|
||||
agent,
|
||||
endpointOption,
|
||||
});
|
||||
agentConfigs.set(agentId, config);
|
||||
}
|
||||
}
|
||||
|
||||
const sender = getResponseSender({
|
||||
...endpointOption,
|
||||
model: endpointOption.model_parameters.model,
|
||||
});
|
||||
const sender =
|
||||
primaryAgent.name ??
|
||||
getResponseSender({
|
||||
...endpointOption,
|
||||
model: endpointOption.model_parameters.model,
|
||||
});
|
||||
|
||||
const client = new AgentClient({
|
||||
req,
|
||||
agent,
|
||||
tools,
|
||||
agent: primaryConfig,
|
||||
sender,
|
||||
attachments,
|
||||
contentParts,
|
||||
modelOptions,
|
||||
eventHandlers,
|
||||
collectedUsage,
|
||||
artifactPromises,
|
||||
spec: endpointOption.spec,
|
||||
agentConfigs,
|
||||
endpoint: EModelEndpoint.agents,
|
||||
attachments: endpointOption.attachments,
|
||||
maxContextTokens:
|
||||
agent.max_context_tokens ??
|
||||
getModelMaxTokens(modelOptions.model, providerEndpointMap[provider]) ??
|
||||
4000,
|
||||
maxContextTokens: primaryConfig.maxContextTokens,
|
||||
});
|
||||
|
||||
return { client };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
const { removeNullishValues } = require('librechat-data-provider');
|
||||
const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
|
||||
const { getAssistant } = require('~/models/Assistant');
|
||||
|
||||
const buildOptions = (endpoint, parsedBody) => {
|
||||
const buildOptions = async (endpoint, parsedBody) => {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
|
||||
parsedBody;
|
||||
|
|
@ -15,6 +16,21 @@ const buildOptions = (endpoint, parsedBody) => {
|
|||
modelOptions,
|
||||
});
|
||||
|
||||
if (assistant_id) {
|
||||
const assistantDoc = await getAssistant({ assistant_id });
|
||||
|
||||
if (assistantDoc) {
|
||||
// Create a clean assistant object with only the needed properties
|
||||
endpointOption.assistant = {
|
||||
append_current_datetime: assistantDoc.append_current_datetime,
|
||||
assistant_id: assistantDoc.assistant_id,
|
||||
conversation_starters: assistantDoc.conversation_starters,
|
||||
createdAt: assistantDoc.createdAt,
|
||||
updatedAt: assistantDoc.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof artifacts === 'string') {
|
||||
endpointOption.artifactsPrompt = generateArtifactsPrompt({ endpoint, artifacts });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
const { removeNullishValues } = require('librechat-data-provider');
|
||||
const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
|
||||
const { getAssistant } = require('~/models/Assistant');
|
||||
|
||||
const buildOptions = (endpoint, parsedBody) => {
|
||||
const buildOptions = async (endpoint, parsedBody) => {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
|
||||
parsedBody;
|
||||
|
|
@ -15,6 +16,19 @@ const buildOptions = (endpoint, parsedBody) => {
|
|||
modelOptions,
|
||||
});
|
||||
|
||||
if (assistant_id) {
|
||||
const assistantDoc = await getAssistant({ assistant_id });
|
||||
if (assistantDoc) {
|
||||
endpointOption.assistant = {
|
||||
append_current_datetime: assistantDoc.append_current_datetime,
|
||||
assistant_id: assistantDoc.assistant_id,
|
||||
conversation_starters: assistantDoc.conversation_starters,
|
||||
createdAt: assistantDoc.createdAt,
|
||||
updatedAt: assistantDoc.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof artifacts === 'string') {
|
||||
endpointOption.artifactsPrompt = generateArtifactsPrompt({ endpoint, artifacts });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,12 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie
|
|||
clientOptions.reverseProxyUrl = baseURL ?? clientOptions.reverseProxyUrl;
|
||||
clientOptions.headers = opts.defaultHeaders;
|
||||
clientOptions.azure = !serverless && azureOptions;
|
||||
if (serverless === true) {
|
||||
clientOptions.defaultQuery = azureOptions.azureOpenAIApiVersion
|
||||
? { 'api-version': azureOptions.azureOpenAIApiVersion }
|
||||
: undefined;
|
||||
clientOptions.headers['api-key'] = apiKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ const {
|
|||
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');
|
||||
|
|
@ -20,8 +19,6 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
const { contentParts, aggregateContent } = createContentAggregator();
|
||||
const eventHandlers = getDefaultHandlers({ res, aggregateContent, collectedUsage });
|
||||
|
||||
// const tools = [createTavilySearchTool()];
|
||||
|
||||
/** @type {Agent} */
|
||||
const agent = {
|
||||
id: EModelEndpoint.bedrock,
|
||||
|
|
@ -36,8 +33,6 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
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,
|
||||
|
|
@ -45,28 +40,34 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
endpointOption,
|
||||
});
|
||||
|
||||
modelOptions = Object.assign(modelOptions, options.llmConfig);
|
||||
const maxContextTokens =
|
||||
agent.max_context_tokens ??
|
||||
getModelMaxTokens(modelOptions.model, providerEndpointMap[agent.provider]);
|
||||
agent.model_parameters = Object.assign(agent.model_parameters, options.llmConfig);
|
||||
if (options.configOptions) {
|
||||
agent.model_parameters.configuration = options.configOptions;
|
||||
}
|
||||
|
||||
const sender = getResponseSender({
|
||||
...endpointOption,
|
||||
model: endpointOption.model_parameters.model,
|
||||
});
|
||||
const sender =
|
||||
agent.name ??
|
||||
getResponseSender({
|
||||
...endpointOption,
|
||||
model: endpointOption.model_parameters.model,
|
||||
});
|
||||
|
||||
const client = new AgentClient({
|
||||
req,
|
||||
agent,
|
||||
sender,
|
||||
// tools,
|
||||
modelOptions,
|
||||
contentParts,
|
||||
eventHandlers,
|
||||
collectedUsage,
|
||||
maxContextTokens,
|
||||
spec: endpointOption.spec,
|
||||
endpoint: EModelEndpoint.bedrock,
|
||||
configOptions: options.configOptions,
|
||||
resendFiles: endpointOption.resendFiles,
|
||||
maxContextTokens:
|
||||
endpointOption.maxContextTokens ??
|
||||
agent.max_context_tokens ??
|
||||
getModelMaxTokens(agent.model_parameters.model, providerEndpointMap[agent.provider]) ??
|
||||
4000,
|
||||
attachments: endpointOption.attachments,
|
||||
});
|
||||
return { client };
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/User
|
|||
const { getLLMConfig } = require('~/server/services/Endpoints/openAI/llm');
|
||||
const { getCustomEndpointConfig } = require('~/server/services/Config');
|
||||
const { fetchModels } = require('~/server/services/ModelService');
|
||||
const { isUserProvided, sleep } = require('~/server/utils');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { isUserProvided } = require('~/server/utils');
|
||||
const { OpenAIClient } = require('~/app');
|
||||
|
||||
const { PROXY } = process.env;
|
||||
|
|
@ -141,7 +141,18 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid
|
|||
},
|
||||
clientOptions,
|
||||
);
|
||||
return getLLMConfig(apiKey, requestOptions);
|
||||
const options = getLLMConfig(apiKey, requestOptions);
|
||||
if (!customOptions.streamRate) {
|
||||
return options;
|
||||
}
|
||||
options.llmConfig.callbacks = [
|
||||
{
|
||||
handleLLMNewToken: async () => {
|
||||
await sleep(customOptions.streamRate);
|
||||
},
|
||||
},
|
||||
];
|
||||
return options;
|
||||
}
|
||||
|
||||
if (clientOptions.reverseProxyUrl) {
|
||||
|
|
|
|||
|
|
@ -96,6 +96,12 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
|
||||
apiKey = azureOptions.azureOpenAIApiKey;
|
||||
clientOptions.azure = !serverless && azureOptions;
|
||||
if (serverless === true) {
|
||||
clientOptions.defaultQuery = azureOptions.azureOpenAIApiVersion
|
||||
? { 'api-version': azureOptions.azureOpenAIApiVersion }
|
||||
: undefined;
|
||||
clientOptions.headers['api-key'] = apiKey;
|
||||
}
|
||||
} else if (useAzure || (apiKey && apiKey.includes('{"azure') && !clientOptions.azure)) {
|
||||
clientOptions.azure = userProvidesKey ? JSON.parse(userValues.apiKey) : getAzureCredentials();
|
||||
apiKey = clientOptions.azure.azureOpenAIApiKey;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const {
|
|||
} = 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 { isEnabled, isUserProvided, sleep } = require('~/server/utils');
|
||||
const { getAzureCredentials } = require('~/utils');
|
||||
const { OpenAIClient } = require('~/app');
|
||||
|
||||
|
|
@ -97,6 +97,12 @@ const initializeClient = async ({
|
|||
|
||||
apiKey = azureOptions.azureOpenAIApiKey;
|
||||
clientOptions.azure = !serverless && azureOptions;
|
||||
if (serverless === true) {
|
||||
clientOptions.defaultQuery = azureOptions.azureOpenAIApiVersion
|
||||
? { 'api-version': azureOptions.azureOpenAIApiVersion }
|
||||
: undefined;
|
||||
clientOptions.headers['api-key'] = apiKey;
|
||||
}
|
||||
} else if (isAzureOpenAI) {
|
||||
clientOptions.azure = userProvidesKey ? JSON.parse(userValues.apiKey) : getAzureCredentials();
|
||||
apiKey = clientOptions.azure.azureOpenAIApiKey;
|
||||
|
|
@ -134,7 +140,18 @@ const initializeClient = async ({
|
|||
},
|
||||
clientOptions,
|
||||
);
|
||||
return getLLMConfig(apiKey, requestOptions);
|
||||
const options = getLLMConfig(apiKey, requestOptions);
|
||||
if (!clientOptions.streamRate) {
|
||||
return options;
|
||||
}
|
||||
options.llmConfig.callbacks = [
|
||||
{
|
||||
handleLLMNewToken: async () => {
|
||||
await sleep(clientOptions.streamRate);
|
||||
},
|
||||
},
|
||||
];
|
||||
return options;
|
||||
}
|
||||
|
||||
const client = new OpenAIClient(apiKey, Object.assign({ req, res }, clientOptions));
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ function getLLMConfig(apiKey, options = {}) {
|
|||
modelOptions = {},
|
||||
reverseProxyUrl,
|
||||
useOpenRouter,
|
||||
defaultQuery,
|
||||
headers,
|
||||
proxy,
|
||||
azure,
|
||||
|
|
@ -74,6 +75,10 @@ function getLLMConfig(apiKey, options = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
if (defaultQuery) {
|
||||
configOptions.baseOptions.defaultQuery = defaultQuery;
|
||||
}
|
||||
|
||||
if (proxy) {
|
||||
const proxyAgent = new HttpsProxyAgent(proxy);
|
||||
Object.assign(configOptions, {
|
||||
|
|
|
|||
|
|
@ -121,9 +121,9 @@ class STTService {
|
|||
*/
|
||||
azureOpenAIProvider(sttSchema, audioBuffer, audioFile) {
|
||||
const url = `${genAzureEndpoint({
|
||||
azureOpenAIApiInstanceName: sttSchema?.instanceName,
|
||||
azureOpenAIApiDeploymentName: sttSchema?.deploymentName,
|
||||
})}/audio/transcriptions?api-version=${sttSchema?.apiVersion}`;
|
||||
azureOpenAIApiInstanceName: extractEnvVariable(sttSchema?.instanceName),
|
||||
azureOpenAIApiDeploymentName: extractEnvVariable(sttSchema?.deploymentName),
|
||||
})}/audio/transcriptions?api-version=${extractEnvVariable(sttSchema?.apiVersion)}`;
|
||||
|
||||
const apiKey = sttSchema.apiKey ? extractEnvVariable(sttSchema.apiKey) : '';
|
||||
|
||||
|
|
|
|||
|
|
@ -143,9 +143,9 @@ class TTSService {
|
|||
*/
|
||||
azureOpenAIProvider(ttsSchema, input, voice) {
|
||||
const url = `${genAzureEndpoint({
|
||||
azureOpenAIApiInstanceName: ttsSchema?.instanceName,
|
||||
azureOpenAIApiDeploymentName: ttsSchema?.deploymentName,
|
||||
})}/audio/speech?api-version=${ttsSchema?.apiVersion}`;
|
||||
azureOpenAIApiInstanceName: extractEnvVariable(ttsSchema?.instanceName),
|
||||
azureOpenAIApiDeploymentName: extractEnvVariable(ttsSchema?.deploymentName),
|
||||
})}/audio/speech?api-version=${extractEnvVariable(ttsSchema?.apiVersion)}`;
|
||||
|
||||
if (
|
||||
ttsSchema?.voices &&
|
||||
|
|
@ -157,7 +157,7 @@ class TTSService {
|
|||
}
|
||||
|
||||
const data = {
|
||||
model: ttsSchema?.model,
|
||||
model: extractEnvVariable(ttsSchema?.model),
|
||||
input,
|
||||
voice: ttsSchema?.voices && ttsSchema.voices.length > 0 ? voice : undefined,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,12 +40,16 @@ async function getCodeOutputDownloadStream(fileIdentifier, apiKey) {
|
|||
* @param {import('fs').ReadStream | import('stream').Readable} params.stream - The read stream for the file.
|
||||
* @param {string} params.filename - The name of the file.
|
||||
* @param {string} params.apiKey - The API key for authentication.
|
||||
* @param {string} [params.entity_id] - Optional entity ID for the file.
|
||||
* @returns {Promise<string>}
|
||||
* @throws {Error} If there's an error during the upload process.
|
||||
*/
|
||||
async function uploadCodeEnvFile({ req, stream, filename, apiKey }) {
|
||||
async function uploadCodeEnvFile({ req, stream, filename, apiKey, entity_id = '' }) {
|
||||
try {
|
||||
const form = new FormData();
|
||||
if (entity_id.length > 0) {
|
||||
form.append('entity_id', entity_id);
|
||||
}
|
||||
form.append('file', stream, filename);
|
||||
|
||||
const baseURL = getCodeBaseURL();
|
||||
|
|
@ -67,7 +71,12 @@ async function uploadCodeEnvFile({ req, stream, filename, apiKey }) {
|
|||
throw new Error(`Error uploading file: ${result.message}`);
|
||||
}
|
||||
|
||||
return `${result.session_id}/${result.files[0].fileId}`;
|
||||
const fileIdentifier = `${result.session_id}/${result.files[0].fileId}`;
|
||||
if (entity_id.length === 0) {
|
||||
return fileIdentifier;
|
||||
}
|
||||
|
||||
return `${fileIdentifier}?entity_id=${entity_id}`;
|
||||
} catch (error) {
|
||||
throw new Error(`Error uploading file: ${error.message}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ const { v4 } = require('uuid');
|
|||
const axios = require('axios');
|
||||
const { getCodeBaseURL } = require('@librechat/agents');
|
||||
const {
|
||||
EToolResources,
|
||||
Tools,
|
||||
FileContext,
|
||||
imageExtRegex,
|
||||
FileSources,
|
||||
imageExtRegex,
|
||||
EToolResources,
|
||||
} = require('librechat-data-provider');
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
const { convertImage } = require('~/server/services/Files/images/convert');
|
||||
|
|
@ -110,12 +111,20 @@ function checkIfActive(dateString) {
|
|||
async function getSessionInfo(fileIdentifier, apiKey) {
|
||||
try {
|
||||
const baseURL = getCodeBaseURL();
|
||||
const session_id = fileIdentifier.split('/')[0];
|
||||
const [path, queryString] = fileIdentifier.split('?');
|
||||
const session_id = path.split('/')[0];
|
||||
|
||||
let queryParams = {};
|
||||
if (queryString) {
|
||||
queryParams = Object.fromEntries(new URLSearchParams(queryString).entries());
|
||||
}
|
||||
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: `${baseURL}/files/${session_id}`,
|
||||
params: {
|
||||
detail: 'summary',
|
||||
...queryParams,
|
||||
},
|
||||
headers: {
|
||||
'User-Agent': 'LibreChat/1.0',
|
||||
|
|
@ -124,7 +133,7 @@ async function getSessionInfo(fileIdentifier, apiKey) {
|
|||
timeout: 5000,
|
||||
});
|
||||
|
||||
return response.data.find((file) => file.name.startsWith(fileIdentifier))?.lastModified;
|
||||
return response.data.find((file) => file.name.startsWith(path))?.lastModified;
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching session info: ${error.message}`, error);
|
||||
return null;
|
||||
|
|
@ -137,29 +146,56 @@ async function getSessionInfo(fileIdentifier, apiKey) {
|
|||
* @param {ServerRequest} options.req
|
||||
* @param {Agent['tool_resources']} options.tool_resources
|
||||
* @param {string} apiKey
|
||||
* @returns {Promise<Array<{ id: string; session_id: string; name: string }>>}
|
||||
* @returns {Promise<{
|
||||
* files: Array<{ id: string; session_id: string; name: string }>,
|
||||
* toolContext: string,
|
||||
* }>}
|
||||
*/
|
||||
const primeFiles = async (options, apiKey) => {
|
||||
const { tool_resources } = options;
|
||||
const file_ids = tool_resources?.[EToolResources.execute_code]?.file_ids ?? [];
|
||||
const dbFiles = await getFiles({ file_id: { $in: file_ids } });
|
||||
const agentResourceIds = new Set(file_ids);
|
||||
const resourceFiles = tool_resources?.[EToolResources.execute_code]?.files ?? [];
|
||||
const dbFiles = ((await getFiles({ file_id: { $in: file_ids } })) ?? []).concat(resourceFiles);
|
||||
|
||||
const files = [];
|
||||
const sessions = new Map();
|
||||
for (const file of dbFiles) {
|
||||
let toolContext = '';
|
||||
|
||||
for (let i = 0; i < dbFiles.length; i++) {
|
||||
const file = dbFiles[i];
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.metadata.fileIdentifier) {
|
||||
const [session_id, id] = file.metadata.fileIdentifier.split('/');
|
||||
const [path, queryString] = file.metadata.fileIdentifier.split('?');
|
||||
const [session_id, id] = path.split('/');
|
||||
|
||||
const pushFile = () => {
|
||||
if (!toolContext) {
|
||||
toolContext = `- Note: The following files are available in the "${Tools.execute_code}" tool environment:`;
|
||||
}
|
||||
toolContext += `\n\t- /mnt/data/${file.filename}${
|
||||
agentResourceIds.has(file.file_id) ? '' : ' (just attached by user)'
|
||||
}`;
|
||||
files.push({
|
||||
id,
|
||||
session_id,
|
||||
name: file.filename,
|
||||
});
|
||||
};
|
||||
|
||||
if (sessions.has(session_id)) {
|
||||
pushFile();
|
||||
continue;
|
||||
}
|
||||
|
||||
let queryParams = {};
|
||||
if (queryString) {
|
||||
queryParams = Object.fromEntries(new URLSearchParams(queryString).entries());
|
||||
}
|
||||
|
||||
const reuploadFile = async () => {
|
||||
try {
|
||||
const { getDownloadStream } = getStrategyFunctions(file.source);
|
||||
|
|
@ -171,6 +207,7 @@ const primeFiles = async (options, apiKey) => {
|
|||
req: options.req,
|
||||
stream,
|
||||
filename: file.filename,
|
||||
entity_id: queryParams.entity_id,
|
||||
apiKey,
|
||||
});
|
||||
await updateFile({ file_id: file.file_id, metadata: { fileIdentifier } });
|
||||
|
|
@ -198,7 +235,7 @@ const primeFiles = async (options, apiKey) => {
|
|||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
return { files, toolContext };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ async function encodeAndFormat(req, files, endpoint, mode) {
|
|||
filepath: file.filepath,
|
||||
filename: file.filename,
|
||||
embedded: !!file.embedded,
|
||||
metadata: file.metadata,
|
||||
};
|
||||
|
||||
if (file.height && file.width) {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const {
|
|||
const { EnvVar } = require('@librechat/agents');
|
||||
const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2');
|
||||
const { convertImage, resizeAndConvert } = require('~/server/services/Files/images');
|
||||
const { addAgentResourceFile, removeAgentResourceFile } = require('~/models/Agent');
|
||||
const { addAgentResourceFile, removeAgentResourceFiles } = require('~/models/Agent');
|
||||
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
||||
const { createFile, updateFileUsage, deleteFiles } = require('~/models/File');
|
||||
const { loadAuthValues } = require('~/app/clients/tools/util');
|
||||
|
|
@ -29,10 +29,34 @@ const { getStrategyFunctions } = require('./strategies');
|
|||
const { determineFileType } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const processFiles = async (files) => {
|
||||
/**
|
||||
*
|
||||
* @param {Array<MongoFile>} files
|
||||
* @param {Array<string>} [fileIds]
|
||||
* @returns
|
||||
*/
|
||||
const processFiles = async (files, fileIds) => {
|
||||
const promises = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (let file of files) {
|
||||
const { file_id } = file;
|
||||
if (seen.has(file_id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(file_id);
|
||||
promises.push(updateFileUsage({ file_id }));
|
||||
}
|
||||
|
||||
if (!fileIds) {
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
for (let file_id of fileIds) {
|
||||
if (seen.has(file_id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(file_id);
|
||||
promises.push(updateFileUsage({ file_id }));
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +68,7 @@ const processFiles = async (files) => {
|
|||
* Enqueues the delete operation to the leaky bucket queue if necessary, or adds it directly to promises.
|
||||
*
|
||||
* @param {object} params - The passed parameters.
|
||||
* @param {Express.Request} params.req - The express request object.
|
||||
* @param {ServerRequest} params.req - The express request object.
|
||||
* @param {MongoFile} params.file - The file object to delete.
|
||||
* @param {Function} params.deleteFile - The delete file function.
|
||||
* @param {Promise[]} params.promises - The array of promises to await.
|
||||
|
|
@ -91,7 +115,7 @@ function enqueueDeleteOperation({ req, file, deleteFile, promises, resolvedFileI
|
|||
*
|
||||
* @param {Object} params - The params object.
|
||||
* @param {MongoFile[]} params.files - The file objects to delete.
|
||||
* @param {Express.Request} params.req - The express request object.
|
||||
* @param {ServerRequest} params.req - The express request object.
|
||||
* @param {DeleteFilesBody} params.req.body - The request body.
|
||||
* @param {string} [params.req.body.agent_id] - The agent ID if file uploaded is associated to an agent.
|
||||
* @param {string} [params.req.body.assistant_id] - The assistant ID if file uploaded is associated to an assistant.
|
||||
|
|
@ -128,18 +152,16 @@ const processDeleteRequest = async ({ req, files }) => {
|
|||
await initializeClients();
|
||||
}
|
||||
|
||||
const agentFiles = [];
|
||||
|
||||
for (const file of files) {
|
||||
const source = file.source ?? FileSources.local;
|
||||
|
||||
if (req.body.agent_id && req.body.tool_resource) {
|
||||
promises.push(
|
||||
removeAgentResourceFile({
|
||||
req,
|
||||
file_id: file.file_id,
|
||||
agent_id: req.body.agent_id,
|
||||
tool_resource: req.body.tool_resource,
|
||||
}),
|
||||
);
|
||||
agentFiles.push({
|
||||
tool_resource: req.body.tool_resource,
|
||||
file_id: file.file_id,
|
||||
});
|
||||
}
|
||||
|
||||
if (checkOpenAIStorage(source) && !client[source]) {
|
||||
|
|
@ -183,6 +205,15 @@ const processDeleteRequest = async ({ req, files }) => {
|
|||
enqueueDeleteOperation({ req, file, deleteFile, promises, resolvedFileIds, openai });
|
||||
}
|
||||
|
||||
if (agentFiles.length > 0) {
|
||||
promises.push(
|
||||
removeAgentResourceFiles({
|
||||
agent_id: req.body.agent_id,
|
||||
files: agentFiles,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
await deleteFiles(resolvedFileIds);
|
||||
};
|
||||
|
|
@ -242,14 +273,14 @@ const processFileURL = async ({ fileStrategy, userId, URL, fileName, basePath, c
|
|||
* Saves file metadata to the database with an expiry TTL.
|
||||
*
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {Express.Request} params.req - The Express request object.
|
||||
* @param {ServerRequest} params.req - The Express request object.
|
||||
* @param {Express.Response} [params.res] - The Express response object.
|
||||
* @param {Express.Multer.File} params.file - The uploaded file.
|
||||
* @param {ImageMetadata} params.metadata - Additional metadata for the file.
|
||||
* @param {boolean} params.returnFile - Whether to return the file metadata or return response as normal.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const processImageFile = async ({ req, res, file, metadata, returnFile = false }) => {
|
||||
const processImageFile = async ({ req, res, metadata, returnFile = false }) => {
|
||||
const { file } = req;
|
||||
const source = req.app.locals.fileStrategy;
|
||||
const { handleImageUpload } = getStrategyFunctions(source);
|
||||
const { file_id, temp_file_id, endpoint } = metadata;
|
||||
|
|
@ -289,7 +320,7 @@ const processImageFile = async ({ req, res, file, metadata, returnFile = false }
|
|||
* returns minimal file metadata, without saving to the database.
|
||||
*
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {Express.Request} params.req - The Express request object.
|
||||
* @param {ServerRequest} params.req - The Express request object.
|
||||
* @param {FileContext} params.context - The context of the file (e.g., 'avatar', 'image_generation', etc.)
|
||||
* @param {boolean} [params.resize=true] - Whether to resize and convert the image to target format. Default is `true`.
|
||||
* @param {{ buffer: Buffer, width: number, height: number, bytes: number, filename: string, type: string, file_id: string }} [params.metadata] - Required metadata for the file if resize is false.
|
||||
|
|
@ -335,13 +366,12 @@ const uploadImageBuffer = async ({ req, context, metadata = {}, resize = true })
|
|||
* Files must be deleted from the server filesystem manually.
|
||||
*
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {Express.Request} params.req - The Express request object.
|
||||
* @param {ServerRequest} params.req - The Express request object.
|
||||
* @param {Express.Response} params.res - The Express response object.
|
||||
* @param {Express.Multer.File} params.file - The uploaded file.
|
||||
* @param {FileMetadata} params.metadata - Additional metadata for the file.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const processFileUpload = async ({ req, res, file, metadata }) => {
|
||||
const processFileUpload = async ({ req, res, metadata }) => {
|
||||
const isAssistantUpload = isAssistantsEndpoint(metadata.endpoint);
|
||||
const assistantSource =
|
||||
metadata.endpoint === EModelEndpoint.azureAssistants ? FileSources.azure : FileSources.openai;
|
||||
|
|
@ -355,6 +385,7 @@ const processFileUpload = async ({ req, res, file, metadata }) => {
|
|||
({ openai } = await getOpenAIClient({ req }));
|
||||
}
|
||||
|
||||
const { file } = req;
|
||||
const {
|
||||
id,
|
||||
bytes,
|
||||
|
|
@ -422,13 +453,13 @@ const processFileUpload = async ({ req, res, file, metadata }) => {
|
|||
* Files must be deleted from the server filesystem manually.
|
||||
*
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {Express.Request} params.req - The Express request object.
|
||||
* @param {ServerRequest} params.req - The Express request object.
|
||||
* @param {Express.Response} params.res - The Express response object.
|
||||
* @param {Express.Multer.File} params.file - The uploaded file.
|
||||
* @param {FileMetadata} params.metadata - Additional metadata for the file.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const processAgentFileUpload = async ({ req, res, file, metadata }) => {
|
||||
const processAgentFileUpload = async ({ req, res, metadata }) => {
|
||||
const { file } = req;
|
||||
const { agent_id, tool_resource } = metadata;
|
||||
if (agent_id && !tool_resource) {
|
||||
throw new Error('No tool resource provided for agent file upload');
|
||||
|
|
@ -453,6 +484,7 @@ const processAgentFileUpload = async ({ req, res, file, metadata }) => {
|
|||
stream,
|
||||
filename: file.originalname,
|
||||
apiKey: result[EnvVar.CODE_API_KEY],
|
||||
entity_id: messageAttachment === true ? undefined : agent_id,
|
||||
});
|
||||
fileInfoMetadata = { fileIdentifier };
|
||||
}
|
||||
|
|
@ -576,7 +608,7 @@ const processOpenAIFile = async ({
|
|||
/**
|
||||
* Process OpenAI image files, convert to target format, save and return file metadata.
|
||||
* @param {object} params - The params object.
|
||||
* @param {Express.Request} params.req - The Express request object.
|
||||
* @param {ServerRequest} params.req - The Express request object.
|
||||
* @param {Buffer} params.buffer - The image buffer.
|
||||
* @param {string} params.file_id - The file ID.
|
||||
* @param {string} params.filename - The filename.
|
||||
|
|
@ -708,20 +740,20 @@ async function retrieveAndProcessFile({
|
|||
* Filters a file based on its size and the endpoint origin.
|
||||
*
|
||||
* @param {Object} params - The parameters for the function.
|
||||
* @param {object} params.req - The request object from Express.
|
||||
* @param {ServerRequest} params.req - The request object from Express.
|
||||
* @param {string} [params.req.endpoint]
|
||||
* @param {string} [params.req.file_id]
|
||||
* @param {number} [params.req.width]
|
||||
* @param {number} [params.req.height]
|
||||
* @param {number} [params.req.version]
|
||||
* @param {Express.Multer.File} params.file - The file uploaded to the server via multer.
|
||||
* @param {boolean} [params.image] - Whether the file expected is an image.
|
||||
* @param {boolean} [params.isAvatar] - Whether the file expected is a user or entity avatar.
|
||||
* @returns {void}
|
||||
*
|
||||
* @throws {Error} If a file exception is caught (invalid file size or type, lack of metadata).
|
||||
*/
|
||||
function filterFile({ req, file, image, isAvatar }) {
|
||||
function filterFile({ req, image, isAvatar }) {
|
||||
const { file } = req;
|
||||
const { endpoint, file_id, width, height } = req.body;
|
||||
|
||||
if (!file_id && !isAvatar) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const { logger } = require('~/config');
|
|||
*
|
||||
* @param {string} userId - The unique identifier of the user for whom the plugin authentication value is to be retrieved.
|
||||
* @param {string} authField - The specific authentication field (e.g., 'API_KEY', 'URL') whose value is to be retrieved and decrypted.
|
||||
* @param {boolean} throwError - Whether to throw an error if the authentication value does not exist. Defaults to `true`.
|
||||
* @returns {Promise<string|null>} A promise that resolves to the decrypted authentication value if found, or `null` if no such authentication value exists for the given user and field.
|
||||
*
|
||||
* The function throws an error if it encounters any issue during the retrieval or decryption process, or if the authentication value does not exist.
|
||||
|
|
@ -22,7 +23,7 @@ const { logger } = require('~/config');
|
|||
* @throws {Error} Throws an error if there's an issue during the retrieval or decryption process, or if the authentication value does not exist.
|
||||
* @async
|
||||
*/
|
||||
const getUserPluginAuthValue = async (userId, authField) => {
|
||||
const getUserPluginAuthValue = async (userId, authField, throwError = true) => {
|
||||
try {
|
||||
const pluginAuth = await PluginAuth.findOne({ userId, authField }).lean();
|
||||
if (!pluginAuth) {
|
||||
|
|
@ -32,6 +33,9 @@ const getUserPluginAuthValue = async (userId, authField) => {
|
|||
const decryptedValue = await decrypt(pluginAuth.value);
|
||||
return decryptedValue;
|
||||
} catch (err) {
|
||||
if (!throwError) {
|
||||
return null;
|
||||
}
|
||||
logger.error('[getUserPluginAuthValue]', err);
|
||||
throw err;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ async function initThread({ openai, body, thread_id: _thread_id }) {
|
|||
thread = await openai.beta.threads.create(body);
|
||||
}
|
||||
|
||||
const thread_id = _thread_id ?? thread.id;
|
||||
const thread_id = _thread_id || thread.id;
|
||||
return { messages, thread_id, ...thread };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { zodToJsonSchema } = require('zod-to-json-schema');
|
||||
const { Calculator } = require('@langchain/community/tools/calculator');
|
||||
const { tool: toolFn, Tool } = require('@langchain/core/tools');
|
||||
const { Calculator } = require('@langchain/community/tools/calculator');
|
||||
const {
|
||||
Tools,
|
||||
ErrorTypes,
|
||||
ContentTypes,
|
||||
imageGenTools,
|
||||
actionDelimiter,
|
||||
|
|
@ -170,7 +171,7 @@ async function processRequiredActions(client, requiredActions) {
|
|||
requiredActions,
|
||||
);
|
||||
const tools = requiredActions.map((action) => action.tool);
|
||||
const loadedTools = await loadTools({
|
||||
const { loadedTools } = await loadTools({
|
||||
user: client.req.user.id,
|
||||
model: client.req.body.model ?? 'gpt-4o-mini',
|
||||
tools,
|
||||
|
|
@ -183,7 +184,6 @@ async function processRequiredActions(client, requiredActions) {
|
|||
fileStrategy: client.req.app.locals.fileStrategy,
|
||||
returnMetadata: true,
|
||||
},
|
||||
skipSpecs: true,
|
||||
});
|
||||
|
||||
const ToolMap = loadedTools.reduce((map, tool) => {
|
||||
|
|
@ -328,6 +328,12 @@ async function processRequiredActions(client, requiredActions) {
|
|||
}
|
||||
|
||||
tool = await createActionTool({ action: actionSet, requestBuilder });
|
||||
if (!tool) {
|
||||
logger.warn(
|
||||
`Invalid action: user: ${client.req.user.id} | thread_id: ${requiredActions[0].thread_id} | run_id: ${requiredActions[0].run_id} | toolName: ${currentAction.tool}`,
|
||||
);
|
||||
throw new Error(`{"type":"${ErrorTypes.INVALID_ACTION}"}`);
|
||||
}
|
||||
isActionTool = !!tool;
|
||||
ActionToolMap[currentAction.tool] = tool;
|
||||
}
|
||||
|
|
@ -378,21 +384,21 @@ async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiK
|
|||
if (!tools || tools.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const loadedTools = await loadTools({
|
||||
const { loadedTools, toolContextMap } = await loadTools({
|
||||
user: req.user.id,
|
||||
// model: req.body.model ?? 'gpt-4o-mini',
|
||||
tools,
|
||||
functions: true,
|
||||
isAgent: agent_id != null,
|
||||
options: {
|
||||
req,
|
||||
openAIApiKey,
|
||||
tool_resources,
|
||||
returnMetadata: true,
|
||||
processFileURL,
|
||||
uploadImageBuffer,
|
||||
returnMetadata: true,
|
||||
fileStrategy: req.app.locals.fileStrategy,
|
||||
},
|
||||
skipSpecs: true,
|
||||
});
|
||||
|
||||
const agentTools = [];
|
||||
|
|
@ -403,16 +409,19 @@ async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiK
|
|||
continue;
|
||||
}
|
||||
|
||||
const toolInstance = toolFn(
|
||||
async (...args) => {
|
||||
return tool['_call'](...args);
|
||||
},
|
||||
{
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
schema: tool.schema,
|
||||
},
|
||||
);
|
||||
const toolDefinition = {
|
||||
name: tool.name,
|
||||
schema: tool.schema,
|
||||
description: tool.description,
|
||||
};
|
||||
|
||||
if (imageGenTools.has(tool.name)) {
|
||||
toolDefinition.responseFormat = 'content_and_artifact';
|
||||
}
|
||||
|
||||
const toolInstance = toolFn(async (...args) => {
|
||||
return tool['_call'](...args);
|
||||
}, toolDefinition);
|
||||
|
||||
agentTools.push(toolInstance);
|
||||
}
|
||||
|
|
@ -462,6 +471,12 @@ async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiK
|
|||
name: toolName,
|
||||
description: functionSig.description,
|
||||
});
|
||||
if (!tool) {
|
||||
logger.warn(
|
||||
`Invalid action: user: ${req.user.id} | agent_id: ${agent_id} | toolName: ${toolName}`,
|
||||
);
|
||||
throw new Error(`{"type":"${ErrorTypes.INVALID_ACTION}"}`);
|
||||
}
|
||||
agentTools.push(tool);
|
||||
ActionToolMap[toolName] = tool;
|
||||
}
|
||||
|
|
@ -476,6 +491,7 @@ async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiK
|
|||
|
||||
return {
|
||||
tools: agentTools,
|
||||
toolContextMap,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
78
api/server/services/createRunBody.js
Normal file
78
api/server/services/createRunBody.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* Obtains the date string in 'YYYY-MM-DD' format.
|
||||
*
|
||||
* @param {string} [clientTimestamp] - Optional ISO timestamp string. If provided, uses this timestamp;
|
||||
* otherwise, uses the current date.
|
||||
* @returns {string} - The date string in 'YYYY-MM-DD' format.
|
||||
*/
|
||||
function getDateStr(clientTimestamp) {
|
||||
return clientTimestamp ? clientTimestamp.split('T')[0] : new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the time string in 'HH:MM:SS' format.
|
||||
*
|
||||
* @param {string} [clientTimestamp] - Optional ISO timestamp string. If provided, uses this timestamp;
|
||||
* otherwise, uses the current time.
|
||||
* @returns {string} - The time string in 'HH:MM:SS' format.
|
||||
*/
|
||||
function getTimeStr(clientTimestamp) {
|
||||
return clientTimestamp
|
||||
? clientTimestamp.split('T')[1].split('.')[0]
|
||||
: new Date().toTimeString().split(' ')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the body object for a run request.
|
||||
*
|
||||
* @param {Object} options - The options for creating the run body.
|
||||
* @param {string} options.assistant_id - The assistant ID.
|
||||
* @param {string} options.model - The model name.
|
||||
* @param {string} [options.promptPrefix] - The prompt prefix to include.
|
||||
* @param {string} [options.instructions] - The instructions to include.
|
||||
* @param {Object} [options.endpointOption={}] - The endpoint options.
|
||||
* @param {string} [options.clientTimestamp] - Client timestamp in ISO format.
|
||||
*
|
||||
* @returns {Object} - The constructed body object for the run request.
|
||||
*/
|
||||
const createRunBody = ({
|
||||
assistant_id,
|
||||
model,
|
||||
promptPrefix,
|
||||
instructions,
|
||||
endpointOption = {},
|
||||
clientTimestamp,
|
||||
}) => {
|
||||
const body = {
|
||||
assistant_id,
|
||||
model,
|
||||
};
|
||||
|
||||
let systemInstructions = '';
|
||||
|
||||
if (endpointOption.assistant?.append_current_datetime) {
|
||||
const dateStr = getDateStr(clientTimestamp);
|
||||
const timeStr = getTimeStr(clientTimestamp);
|
||||
systemInstructions = `Current date and time: ${dateStr} ${timeStr}\n`;
|
||||
}
|
||||
|
||||
if (promptPrefix) {
|
||||
systemInstructions += promptPrefix;
|
||||
}
|
||||
|
||||
if (typeof endpointOption?.artifactsPrompt === 'string' && endpointOption.artifactsPrompt) {
|
||||
systemInstructions += `\n${endpointOption.artifactsPrompt}`;
|
||||
}
|
||||
|
||||
if (systemInstructions.trim()) {
|
||||
body.additional_instructions = systemInstructions.trim();
|
||||
}
|
||||
|
||||
if (instructions) {
|
||||
body.instructions = instructions;
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
module.exports = { createRunBody, getDateStr, getTimeStr };
|
||||
109
api/server/services/domains.js
Normal file
109
api/server/services/domains.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
const { getCustomConfig } = require('~/server/services/Config');
|
||||
|
||||
/**
|
||||
* @param {string} email
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function isEmailDomainAllowed(email) {
|
||||
if (!email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const domain = email.split('@')[1];
|
||||
|
||||
if (!domain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const customConfig = await getCustomConfig();
|
||||
if (!customConfig) {
|
||||
return true;
|
||||
} else if (!customConfig?.registration?.allowedDomains) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return customConfig.registration.allowedDomains.includes(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a domain string
|
||||
* @param {string} domain
|
||||
* @returns {string|null}
|
||||
*/
|
||||
/**
|
||||
* Normalizes a domain string. If the domain is invalid, returns null.
|
||||
* Normalized === lowercase, trimmed, and protocol added if missing.
|
||||
* @param {string} domain
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function normalizeDomain(domain) {
|
||||
try {
|
||||
let normalizedDomain = domain.toLowerCase().trim();
|
||||
|
||||
// Early return for obviously invalid formats
|
||||
if (normalizedDomain === 'http://' || normalizedDomain === 'https://') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If it's not already a URL, make it one
|
||||
if (!normalizedDomain.startsWith('http://') && !normalizedDomain.startsWith('https://')) {
|
||||
normalizedDomain = `https://${normalizedDomain}`;
|
||||
}
|
||||
|
||||
const url = new URL(normalizedDomain);
|
||||
// Additional validation that hostname isn't just protocol
|
||||
if (!url.hostname || url.hostname === 'http:' || url.hostname === 'https:') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return url.hostname.replace(/^www\./i, '');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given domain is allowed. If no restrictions are set, allows all domains.
|
||||
* @param {string} [domain]
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function isActionDomainAllowed(domain) {
|
||||
if (!domain || typeof domain !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const customConfig = await getCustomConfig();
|
||||
const allowedDomains = customConfig?.actions?.allowedDomains;
|
||||
|
||||
if (!Array.isArray(allowedDomains) || !allowedDomains.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const normalizedInputDomain = normalizeDomain(domain);
|
||||
if (!normalizedInputDomain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const allowedDomain of allowedDomains) {
|
||||
const normalizedAllowedDomain = normalizeDomain(allowedDomain);
|
||||
if (!normalizedAllowedDomain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (normalizedAllowedDomain.startsWith('*.')) {
|
||||
const baseDomain = normalizedAllowedDomain.slice(2);
|
||||
if (
|
||||
normalizedInputDomain === baseDomain ||
|
||||
normalizedInputDomain.endsWith(`.${baseDomain}`)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} else if (normalizedInputDomain === normalizedAllowedDomain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = { isEmailDomainAllowed, isActionDomainAllowed };
|
||||
193
api/server/services/domains.spec.js
Normal file
193
api/server/services/domains.spec.js
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
const { isEmailDomainAllowed, isActionDomainAllowed } = require('~/server/services/domains');
|
||||
const { getCustomConfig } = require('~/server/services/Config');
|
||||
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
getCustomConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('isEmailDomainAllowed', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return false if email is falsy', async () => {
|
||||
const email = '';
|
||||
const result = await isEmailDomainAllowed(email);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if domain is not present in the email', async () => {
|
||||
const email = 'test';
|
||||
const result = await isEmailDomainAllowed(email);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if customConfig is not available', async () => {
|
||||
const email = 'test@domain1.com';
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
const result = await isEmailDomainAllowed(email);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if allowedDomains is not defined in customConfig', async () => {
|
||||
const email = 'test@domain1.com';
|
||||
getCustomConfig.mockResolvedValue({});
|
||||
const result = await isEmailDomainAllowed(email);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if domain is included in the allowedDomains', async () => {
|
||||
const email = 'user@domain1.com';
|
||||
getCustomConfig.mockResolvedValue({
|
||||
registration: {
|
||||
allowedDomains: ['domain1.com', 'domain2.com'],
|
||||
},
|
||||
});
|
||||
const result = await isEmailDomainAllowed(email);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if domain is not included in the allowedDomains', async () => {
|
||||
const email = 'user@domain3.com';
|
||||
getCustomConfig.mockResolvedValue({
|
||||
registration: {
|
||||
allowedDomains: ['domain1.com', 'domain2.com'],
|
||||
},
|
||||
});
|
||||
const result = await isEmailDomainAllowed(email);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isActionDomainAllowed', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// Basic Input Validation Tests
|
||||
describe('input validation', () => {
|
||||
it('should return false for falsy values', async () => {
|
||||
expect(await isActionDomainAllowed()).toBe(false);
|
||||
expect(await isActionDomainAllowed(null)).toBe(false);
|
||||
expect(await isActionDomainAllowed('')).toBe(false);
|
||||
expect(await isActionDomainAllowed(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for non-string inputs', async () => {
|
||||
expect(await isActionDomainAllowed(123)).toBe(false);
|
||||
expect(await isActionDomainAllowed({})).toBe(false);
|
||||
expect(await isActionDomainAllowed([])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for invalid domain formats', async () => {
|
||||
getCustomConfig.mockResolvedValue({
|
||||
actions: { allowedDomains: ['http://', 'https://'] },
|
||||
});
|
||||
expect(await isActionDomainAllowed('http://')).toBe(false);
|
||||
expect(await isActionDomainAllowed('https://')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Configuration Tests
|
||||
describe('configuration handling', () => {
|
||||
it('should return true if customConfig is null', async () => {
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
expect(await isActionDomainAllowed('example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if actions.allowedDomains is not defined', async () => {
|
||||
getCustomConfig.mockResolvedValue({});
|
||||
expect(await isActionDomainAllowed('example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if allowedDomains is empty array', async () => {
|
||||
getCustomConfig.mockResolvedValue({
|
||||
actions: { allowedDomains: [] },
|
||||
});
|
||||
expect(await isActionDomainAllowed('example.com')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Domain Matching Tests
|
||||
describe('domain matching', () => {
|
||||
beforeEach(() => {
|
||||
getCustomConfig.mockResolvedValue({
|
||||
actions: {
|
||||
allowedDomains: [
|
||||
'example.com',
|
||||
'*.subdomain.com',
|
||||
'specific.domain.com',
|
||||
'www.withprefix.com',
|
||||
'swapi.dev',
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should match exact domains', async () => {
|
||||
expect(await isActionDomainAllowed('example.com')).toBe(true);
|
||||
expect(await isActionDomainAllowed('other.com')).toBe(false);
|
||||
expect(await isActionDomainAllowed('swapi.dev')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle domains with www prefix', async () => {
|
||||
expect(await isActionDomainAllowed('www.example.com')).toBe(true);
|
||||
expect(await isActionDomainAllowed('www.withprefix.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle full URLs', async () => {
|
||||
expect(await isActionDomainAllowed('https://example.com')).toBe(true);
|
||||
expect(await isActionDomainAllowed('http://example.com')).toBe(true);
|
||||
expect(await isActionDomainAllowed('https://example.com/path')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle wildcard subdomains', async () => {
|
||||
expect(await isActionDomainAllowed('test.subdomain.com')).toBe(true);
|
||||
expect(await isActionDomainAllowed('any.subdomain.com')).toBe(true);
|
||||
expect(await isActionDomainAllowed('subdomain.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle specific subdomains', async () => {
|
||||
expect(await isActionDomainAllowed('specific.domain.com')).toBe(true);
|
||||
expect(await isActionDomainAllowed('other.domain.com')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Edge Cases
|
||||
describe('edge cases', () => {
|
||||
beforeEach(() => {
|
||||
getCustomConfig.mockResolvedValue({
|
||||
actions: {
|
||||
allowedDomains: ['example.com', '*.test.com'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle domains with query parameters', async () => {
|
||||
expect(await isActionDomainAllowed('example.com?param=value')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle domains with ports', async () => {
|
||||
expect(await isActionDomainAllowed('example.com:8080')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle domains with trailing slashes', async () => {
|
||||
expect(await isActionDomainAllowed('example.com/')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle case insensitivity', async () => {
|
||||
expect(await isActionDomainAllowed('EXAMPLE.COM')).toBe(true);
|
||||
expect(await isActionDomainAllowed('Example.Com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle invalid entries in allowedDomains', async () => {
|
||||
getCustomConfig.mockResolvedValue({
|
||||
actions: {
|
||||
allowedDomains: ['example.com', null, undefined, '', 'test.com'],
|
||||
},
|
||||
});
|
||||
expect(await isActionDomainAllowed('example.com')).toBe(true);
|
||||
expect(await isActionDomainAllowed('test.com')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
const { getCustomConfig } = require('~/server/services/Config');
|
||||
|
||||
async function isDomainAllowed(email) {
|
||||
if (!email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const domain = email.split('@')[1];
|
||||
|
||||
if (!domain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const customConfig = await getCustomConfig();
|
||||
if (!customConfig) {
|
||||
return true;
|
||||
} else if (!customConfig?.registration?.allowedDomains) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return customConfig.registration.allowedDomains.includes(domain);
|
||||
}
|
||||
|
||||
module.exports = isDomainAllowed;
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
const { getCustomConfig } = require('~/server/services/Config');
|
||||
const isDomainAllowed = require('./isDomainAllowed');
|
||||
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
getCustomConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('isDomainAllowed', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return false if email is falsy', async () => {
|
||||
const email = '';
|
||||
const result = await isDomainAllowed(email);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if domain is not present in the email', async () => {
|
||||
const email = 'test';
|
||||
const result = await isDomainAllowed(email);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if customConfig is not available', async () => {
|
||||
const email = 'test@domain1.com';
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
const result = await isDomainAllowed(email);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if allowedDomains is not defined in customConfig', async () => {
|
||||
const email = 'test@domain1.com';
|
||||
getCustomConfig.mockResolvedValue({});
|
||||
const result = await isDomainAllowed(email);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if domain is included in the allowedDomains', async () => {
|
||||
const email = 'user@domain1.com';
|
||||
getCustomConfig.mockResolvedValue({
|
||||
registration: {
|
||||
allowedDomains: ['domain1.com', 'domain2.com'],
|
||||
},
|
||||
});
|
||||
const result = await isDomainAllowed(email);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if domain is not included in the allowedDomains', async () => {
|
||||
const email = 'user@domain3.com';
|
||||
getCustomConfig.mockResolvedValue({
|
||||
registration: {
|
||||
allowedDomains: ['domain1.com', 'domain2.com'],
|
||||
},
|
||||
});
|
||||
const result = await isDomainAllowed(email);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
14
api/server/services/start/agents.js
Normal file
14
api/server/services/start/agents.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const { EModelEndpoint, agentsEndpointSChema } = require('librechat-data-provider');
|
||||
|
||||
/**
|
||||
* Sets up the Agents configuration from the config (`librechat.yaml`) file.
|
||||
* @param {TCustomConfig} config - The loaded custom configuration.
|
||||
* @returns {Partial<TAgentsEndpoint>} The Agents endpoint configuration.
|
||||
*/
|
||||
function agentsConfigSetup(config) {
|
||||
const agentsConfig = config.endpoints[EModelEndpoint.agents];
|
||||
const parsedConfig = agentsEndpointSChema.parse(agentsConfig);
|
||||
return parsedConfig;
|
||||
}
|
||||
|
||||
module.exports = { agentsConfigSetup };
|
||||
|
|
@ -32,17 +32,20 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
|
|||
bookmarks: interfaceConfig?.bookmarks ?? defaults.bookmarks,
|
||||
prompts: interfaceConfig?.prompts ?? defaults.prompts,
|
||||
multiConvo: interfaceConfig?.multiConvo ?? defaults.multiConvo,
|
||||
agents: interfaceConfig?.agents ?? defaults.agents,
|
||||
});
|
||||
|
||||
await updateAccessPermissions(roleName, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: loadedInterface.agents },
|
||||
});
|
||||
await updateAccessPermissions(SystemRoles.ADMIN, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: loadedInterface.agents },
|
||||
});
|
||||
|
||||
let i = 0;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,15 @@ jest.mock('~/models/Role', () => ({
|
|||
}));
|
||||
|
||||
describe('loadDefaultInterface', () => {
|
||||
it('should call updateAccessPermissions with the correct parameters when prompts and bookmarks are true', async () => {
|
||||
const config = { interface: { prompts: true, bookmarks: true } };
|
||||
it('should call updateAccessPermissions with the correct parameters when permission types are true', async () => {
|
||||
const config = {
|
||||
interface: {
|
||||
prompts: true,
|
||||
bookmarks: true,
|
||||
multiConvo: true,
|
||||
agents: true,
|
||||
},
|
||||
};
|
||||
const configDefaults = { interface: {} };
|
||||
|
||||
await loadDefaultInterface(config, configDefaults);
|
||||
|
|
@ -16,12 +23,20 @@ describe('loadDefaultInterface', () => {
|
|||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call updateAccessPermissions with false when prompts and bookmarks are false', async () => {
|
||||
const config = { interface: { prompts: false, bookmarks: false } };
|
||||
it('should call updateAccessPermissions with false when permission types are false', async () => {
|
||||
const config = {
|
||||
interface: {
|
||||
prompts: false,
|
||||
bookmarks: false,
|
||||
multiConvo: false,
|
||||
agents: false,
|
||||
},
|
||||
};
|
||||
const configDefaults = { interface: {} };
|
||||
|
||||
await loadDefaultInterface(config, configDefaults);
|
||||
|
|
@ -29,11 +44,12 @@ describe('loadDefaultInterface', () => {
|
|||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call updateAccessPermissions with undefined when prompts and bookmarks are not specified in config', async () => {
|
||||
it('should call updateAccessPermissions with undefined when permission types are not specified in config', async () => {
|
||||
const config = {};
|
||||
const configDefaults = { interface: {} };
|
||||
|
||||
|
|
@ -43,11 +59,19 @@ describe('loadDefaultInterface', () => {
|
|||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call updateAccessPermissions with undefined when prompts and bookmarks are explicitly undefined', async () => {
|
||||
const config = { interface: { prompts: undefined, bookmarks: undefined } };
|
||||
it('should call updateAccessPermissions with undefined when permission types are explicitly undefined', async () => {
|
||||
const config = {
|
||||
interface: {
|
||||
prompts: undefined,
|
||||
bookmarks: undefined,
|
||||
multiConvo: undefined,
|
||||
agents: undefined,
|
||||
},
|
||||
};
|
||||
const configDefaults = { interface: {} };
|
||||
|
||||
await loadDefaultInterface(config, configDefaults);
|
||||
|
|
@ -56,11 +80,19 @@ describe('loadDefaultInterface', () => {
|
|||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call updateAccessPermissions with mixed values for prompts and bookmarks', async () => {
|
||||
const config = { interface: { prompts: true, bookmarks: false } };
|
||||
it('should call updateAccessPermissions with mixed values for permission types', async () => {
|
||||
const config = {
|
||||
interface: {
|
||||
prompts: true,
|
||||
bookmarks: false,
|
||||
multiConvo: undefined,
|
||||
agents: true,
|
||||
},
|
||||
};
|
||||
const configDefaults = { interface: {} };
|
||||
|
||||
await loadDefaultInterface(config, configDefaults);
|
||||
|
|
@ -69,19 +101,28 @@ describe('loadDefaultInterface', () => {
|
|||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call updateAccessPermissions with true when config is undefined', async () => {
|
||||
const config = undefined;
|
||||
const configDefaults = { interface: { prompts: true, bookmarks: true } };
|
||||
const configDefaults = {
|
||||
interface: {
|
||||
prompts: true,
|
||||
bookmarks: true,
|
||||
multiConvo: true,
|
||||
agents: true,
|
||||
},
|
||||
};
|
||||
|
||||
await loadDefaultInterface(config, configDefaults);
|
||||
|
||||
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: true },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -95,6 +136,7 @@ describe('loadDefaultInterface', () => {
|
|||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -108,6 +150,7 @@ describe('loadDefaultInterface', () => {
|
|||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -121,11 +164,19 @@ describe('loadDefaultInterface', () => {
|
|||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call updateAccessPermissions with all interface options including multiConvo', async () => {
|
||||
const config = { interface: { prompts: true, bookmarks: false, multiConvo: true } };
|
||||
const config = {
|
||||
interface: {
|
||||
prompts: true,
|
||||
bookmarks: false,
|
||||
multiConvo: true,
|
||||
agents: false,
|
||||
},
|
||||
};
|
||||
const configDefaults = { interface: {} };
|
||||
|
||||
await loadDefaultInterface(config, configDefaults);
|
||||
|
|
@ -134,12 +185,20 @@ describe('loadDefaultInterface', () => {
|
|||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default values for multiConvo when config is undefined', async () => {
|
||||
const config = undefined;
|
||||
const configDefaults = { interface: { prompts: true, bookmarks: true, multiConvo: false } };
|
||||
const configDefaults = {
|
||||
interface: {
|
||||
prompts: true,
|
||||
bookmarks: true,
|
||||
multiConvo: false,
|
||||
agents: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
await loadDefaultInterface(config, configDefaults);
|
||||
|
||||
|
|
@ -147,6 +206,7 @@ describe('loadDefaultInterface', () => {
|
|||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -196,14 +196,11 @@ function generateConfig(key, baseURL, endpoint) {
|
|||
|
||||
if (agents) {
|
||||
config.capabilities = [
|
||||
AgentCapabilities.execute_code,
|
||||
AgentCapabilities.file_search,
|
||||
AgentCapabilities.actions,
|
||||
AgentCapabilities.tools,
|
||||
];
|
||||
|
||||
if (key === 'EXPERIMENTAL_RUN_CODE') {
|
||||
config.capabilities.push(AgentCapabilities.execute_code);
|
||||
}
|
||||
}
|
||||
|
||||
if (assistants && endpoint === EModelEndpoint.azureAssistants) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
const fs = require('fs');
|
||||
const LdapStrategy = require('passport-ldapauth');
|
||||
const { SystemRoles } = require('librechat-data-provider');
|
||||
const { findUser, createUser, updateUser } = require('~/models/userMethods');
|
||||
const { countUsers } = require('~/models/userMethods');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const logger = require('~/utils/logger');
|
||||
|
||||
|
|
@ -109,6 +111,7 @@ const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => {
|
|||
}
|
||||
|
||||
if (!user) {
|
||||
const isFirstRegisteredUser = (await countUsers()) === 0;
|
||||
user = {
|
||||
provider: 'ldap',
|
||||
ldapId,
|
||||
|
|
@ -116,6 +119,7 @@ const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => {
|
|||
email: mail,
|
||||
emailVerified: true, // The ldap server administrator should verify the email
|
||||
name: fullName,
|
||||
role: isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER,
|
||||
};
|
||||
const userId = await createUser(user);
|
||||
user._id = userId;
|
||||
|
|
|
|||
|
|
@ -56,12 +56,33 @@
|
|||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports BaseMessage
|
||||
* @typedef {import('@langchain/core/messages').BaseMessage} BaseMessage
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports UsageMetadata
|
||||
* @typedef {import('@langchain/core/messages').UsageMetadata} UsageMetadata
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports GraphRunnableConfig
|
||||
* @typedef {import('@langchain/core/runnables').RunnableConfig<{
|
||||
* req: ServerRequest;
|
||||
* thread_id: string;
|
||||
* run_id: string;
|
||||
* agent_id: string;
|
||||
* name: string;
|
||||
* agent_index: number;
|
||||
* last_agent_index: number;
|
||||
* hide_sequential_outputs: boolean;
|
||||
* }>} GraphRunnableConfig
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports Ollama
|
||||
* @typedef {import('ollama').Ollama} Ollama
|
||||
|
|
@ -689,6 +710,12 @@
|
|||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports ToolCallData
|
||||
* @typedef {import('~/models/schema/toolCallSchema.js').ToolCallData} ToolCallData
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports MongoUser
|
||||
* @typedef {import('~/models/schema/userSchema.js').MongoUser} MongoUser
|
||||
|
|
@ -797,12 +824,24 @@
|
|||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports TAgentsEndpoint
|
||||
* @typedef {import('librechat-data-provider').TAgentsEndpoint} TAgentsEndpoint
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports Agent
|
||||
* @typedef {import('librechat-data-provider').Agent} Agent
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports AgentToolResources
|
||||
* @typedef {import('librechat-data-provider').AgentToolResources} AgentToolResources
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports AgentCreateParams
|
||||
* @typedef {import('librechat-data-provider').AgentCreateParams} AgentCreateParams
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ const googleModels = {
|
|||
gemini: 30720, // -2048 from max
|
||||
'gemini-pro-vision': 12288, // -4096 from max
|
||||
'gemini-exp': 8000,
|
||||
'gemini-1.5': 1048576, // -8192 from max
|
||||
'gemini-2.0': 1048576,
|
||||
'gemini-1.5': 1048576,
|
||||
'text-bison-32k': 32758, // -10 from max
|
||||
'chat-bison-32k': 32758, // -10 from max
|
||||
'code-bison-32k': 32758, // -10 from max
|
||||
|
|
@ -117,6 +118,10 @@ const amazonModels = {
|
|||
'amazon.titan-text-lite-v1': 4000,
|
||||
'amazon.titan-text-express-v1': 8000,
|
||||
'amazon.titan-text-premier-v1:0': 31500, // -500 from max
|
||||
// https://aws.amazon.com/ai/generative-ai/nova/
|
||||
'amazon.nova-micro-v1:0': 127000, // -1000 from max,
|
||||
'amazon.nova-lite-v1:0': 295000, // -5000 from max,
|
||||
'amazon.nova-pro-v1:0': 295000, // -5000 from max,
|
||||
};
|
||||
|
||||
const bedrockModels = {
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@
|
|||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-supersub": "^1.0.0",
|
||||
"sse.js": "^2.5.0",
|
||||
"tailwind-merge": "^1.9.1",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"tailwindcss-radix": "^2.8.0",
|
||||
|
|
|
|||
1
client/public/assets/c.svg
Normal file
1
client/public/assets/c.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>C</title><path d="M16.5921 9.1962s-.354-3.298-3.627-3.39c-3.2741-.09-4.9552 2.474-4.9552 6.14 0 3.6651 1.858 6.5972 5.0451 6.5972 3.184 0 3.5381-3.665 3.5381-3.665l6.1041.365s.36 3.31-2.196 5.836c-2.552 2.5241-5.6901 2.9371-7.8762 2.9201-2.19-.017-5.2261.034-8.1602-2.97-2.938-3.0101-3.436-5.9302-3.436-8.8002 0-2.8701.556-6.6702 4.047-9.5502C7.444.72 9.849 0 12.254 0c10.0422 0 10.7172 9.2602 10.7172 9.2602z"/></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
1
client/public/assets/cplusplus.svg
Normal file
1
client/public/assets/cplusplus.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>C++</title><path d="M22.394 6c-.167-.29-.398-.543-.652-.69L12.926.22c-.509-.294-1.34-.294-1.848 0L2.26 5.31c-.508.293-.923 1.013-.923 1.6v10.18c0 .294.104.62.271.91.167.29.398.543.652.69l8.816 5.09c.508.293 1.34.293 1.848 0l8.816-5.09c.254-.147.485-.4.652-.69.167-.29.27-.616.27-.91V6.91c.003-.294-.1-.62-.268-.91zM12 19.11c-3.92 0-7.109-3.19-7.109-7.11 0-3.92 3.19-7.11 7.11-7.11a7.133 7.133 0 016.156 3.553l-3.076 1.78a3.567 3.567 0 00-3.08-1.78A3.56 3.56 0 008.444 12 3.56 3.56 0 0012 15.555a3.57 3.57 0 003.08-1.778l3.078 1.78A7.135 7.135 0 0112 19.11zm7.11-6.715h-.79v.79h-.79v-.79h-.79v-.79h.79v-.79h.79v.79h.79zm2.962 0h-.79v.79h-.79v-.79h-.79v-.79h.79v-.79h.79v.79h.79z"/></svg>
|
||||
|
After Width: | Height: | Size: 764 B |
1
client/public/assets/fortran.svg
Normal file
1
client/public/assets/fortran.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Fortran</title><path d="M19.536 0H4.464A4.463 4.463 0 0 0 0 4.464v15.073A4.463 4.463 0 0 0 4.464 24h15.073A4.463 4.463 0 0 0 24 19.536V4.464A4.463 4.463 0 0 0 19.536 0zm1.193 6.493v3.871l-.922-.005c-.507-.003-.981-.021-1.052-.041-.128-.036-.131-.05-.192-.839-.079-1.013-.143-1.462-.306-2.136-.352-1.457-1.096-2.25-2.309-2.463-.509-.089-2.731-.176-4.558-.177L10.13 4.7v5.82l.662-.033c.757-.038 1.353-.129 1.64-.252.306-.131.629-.462.781-.799.158-.352.262-.815.345-1.542.033-.286.07-.572.083-.636.024-.116.028-.117 1.036-.117h1.012v9.3h-2.062l-.035-.536c-.063-.971-.252-1.891-.479-2.331-.311-.601-.922-.871-2.151-.95a11.422 11.422 0 0 1-.666-.059l-.172-.027.02 2.926c.021 3.086.03 3.206.265 3.465.241.266.381.284 2.827.368.05.002.065.246.065 1.041v1.039H3.271v-1.039c0-.954.007-1.039.091-1.041.05-.001.543-.023 1.097-.049.891-.042 1.033-.061 1.244-.167a.712.712 0 0 0 .345-.328c.106-.206.107-.254.107-6.78 0-6.133-.006-6.584-.09-6.737a.938.938 0 0 0-.553-.436c-.104-.032-.65-.07-1.215-.086l-1.026-.027V2.622h17.458v3.871z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
client/public/assets/go.svg
Normal file
1
client/public/assets/go.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Go</title><path d="M1.811 10.231c-.047 0-.058-.023-.035-.059l.246-.315c.023-.035.081-.058.128-.058h4.172c.046 0 .058.035.035.07l-.199.303c-.023.036-.082.07-.117.07zM.047 11.306c-.047 0-.059-.023-.035-.058l.245-.316c.023-.035.082-.058.129-.058h5.328c.047 0 .07.035.058.07l-.093.28c-.012.047-.058.07-.105.07zm2.828 1.075c-.047 0-.059-.035-.035-.07l.163-.292c.023-.035.07-.07.117-.07h2.337c.047 0 .07.035.07.082l-.023.28c0 .047-.047.082-.082.082zm12.129-2.36c-.736.187-1.239.327-1.963.514-.176.046-.187.058-.34-.117-.174-.199-.303-.327-.548-.444-.737-.362-1.45-.257-2.115.175-.795.514-1.204 1.274-1.192 2.22.011.935.654 1.706 1.577 1.835.795.105 1.46-.175 1.987-.77.105-.13.198-.27.315-.434H10.47c-.245 0-.304-.152-.222-.35.152-.362.432-.97.596-1.274a.315.315 0 01.292-.187h4.253c-.023.316-.023.631-.07.947a4.983 4.983 0 01-.958 2.29c-.841 1.11-1.94 1.8-3.33 1.986-1.145.152-2.209-.07-3.143-.77-.865-.655-1.356-1.52-1.484-2.595-.152-1.274.222-2.419.993-3.424.83-1.086 1.928-1.776 3.272-2.02 1.098-.2 2.15-.07 3.096.571.62.41 1.063.97 1.356 1.648.07.105.023.164-.117.2m3.868 6.461c-1.064-.024-2.034-.328-2.852-1.029a3.665 3.665 0 01-1.262-2.255c-.21-1.32.152-2.489.947-3.529.853-1.122 1.881-1.706 3.272-1.95 1.192-.21 2.314-.095 3.33.595.923.63 1.496 1.484 1.648 2.605.198 1.578-.257 2.863-1.344 3.962-.771.783-1.718 1.273-2.805 1.495-.315.06-.63.07-.934.106zm2.78-4.72c-.011-.153-.011-.27-.034-.387-.21-1.157-1.274-1.81-2.384-1.554-1.087.245-1.788.935-2.045 2.033-.21.912.234 1.835 1.075 2.21.643.28 1.285.244 1.905-.07.923-.48 1.425-1.228 1.484-2.233z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
client/public/assets/nodedotjs.svg
Normal file
1
client/public/assets/nodedotjs.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Node.js</title><path d="M11.998,24c-0.321,0-0.641-0.084-0.922-0.247l-2.936-1.737c-0.438-0.245-0.224-0.332-0.08-0.383 c0.585-0.203,0.703-0.25,1.328-0.604c0.065-0.037,0.151-0.023,0.218,0.017l2.256,1.339c0.082,0.045,0.197,0.045,0.272,0l8.795-5.076 c0.082-0.047,0.134-0.141,0.134-0.238V6.921c0-0.099-0.053-0.192-0.137-0.242l-8.791-5.072c-0.081-0.047-0.189-0.047-0.271,0 L3.075,6.68C2.99,6.729,2.936,6.825,2.936,6.921v10.15c0,0.097,0.054,0.189,0.139,0.235l2.409,1.392 c1.307,0.654,2.108-0.116,2.108-0.89V7.787c0-0.142,0.114-0.253,0.256-0.253h1.115c0.139,0,0.255,0.112,0.255,0.253v10.021 c0,1.745-0.95,2.745-2.604,2.745c-0.508,0-0.909,0-2.026-0.551L2.28,18.675c-0.57-0.329-0.922-0.945-0.922-1.604V6.921 c0-0.659,0.353-1.275,0.922-1.603l8.795-5.082c0.557-0.315,1.296-0.315,1.848,0l8.794,5.082c0.57,0.329,0.924,0.944,0.924,1.603 v10.15c0,0.659-0.354,1.273-0.924,1.604l-8.794,5.078C12.643,23.916,12.324,24,11.998,24z M19.099,13.993 c0-1.9-1.284-2.406-3.987-2.763c-2.731-0.361-3.009-0.548-3.009-1.187c0-0.528,0.235-1.233,2.258-1.233 c1.807,0,2.473,0.389,2.747,1.607c0.024,0.115,0.129,0.199,0.247,0.199h1.141c0.071,0,0.138-0.031,0.186-0.081 c0.048-0.054,0.074-0.123,0.067-0.196c-0.177-2.098-1.571-3.076-4.388-3.076c-2.508,0-4.004,1.058-4.004,2.833 c0,1.925,1.488,2.457,3.895,2.695c2.88,0.282,3.103,0.703,3.103,1.269c0,0.983-0.789,1.402-2.642,1.402 c-2.327,0-2.839-0.584-3.011-1.742c-0.02-0.124-0.126-0.215-0.253-0.215h-1.137c-0.141,0-0.254,0.112-0.254,0.253 c0,1.482,0.806,3.248,4.655,3.248C17.501,17.007,19.099,15.91,19.099,13.993z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue