Skip to content

Commit

Permalink
🎉 feat: Code Interpreter API and Agents Release (#4860)
Browse files Browse the repository at this point in the history
* feat: Code Interpreter API & File Search Agent Uploads

chore: add back code files

wip: first pass, abstract key dialog

refactor: influence checkbox on key changes

refactor: update localization keys for 'execute code' to 'run code'

wip: run code button

refactor: add throwError parameter to loadAuthValues and getUserPluginAuthValue functions

feat: first pass, API tool calling

fix: handle missing toolId in callTool function and return 404 for non-existent tools

feat: show code outputs

fix: improve error handling in callTool function and log errors

fix: handle potential null value for filepath in attachment destructuring

fix: normalize language before rendering and prevent null return

fix: add loading indicator in RunCode component while executing code

feat: add support for conditional code execution in Markdown components

feat: attachments

refactor: remove bash

fix: pass abort signal to graph/run

refactor: debounce and rate limit tool call

refactor: increase debounce delay for execute function

feat: set code output attachments

feat: image attachments

refactor: apply message context

refactor: pass `partIndex`

feat: toolCall schema/model/methods

feat: block indexing

feat: get tool calls

chore: imports

chore: typing

chore: condense type imports

feat: get tool calls

fix: block indexing

chore: typing

refactor: update tool calls mapping to support multiple results

fix: add unique key to nav link for rendering

wip: first pass, tool call results

refactor: update query cache from successful tool call mutation

style: improve result switcher styling

chore: note on using \`.toObject()\`

feat: add agent_id field to conversation schema

chore: typing

refactor: rename agentMap to agentsMap for consistency

feat: Agent Name as chat input placeholder

chore: bump agents

📦 chore: update @langchain dependencies to latest versions to match agents package

📦 chore: update @librechat/agents dependency to version 1.8.0

fix: Aborting agent stream removes sender; fix(bedrock): completion removes preset name label

refactor: remove direct file parameter to use req.file, add `processAgentFileUpload` for image uploads

feat: upload menu

feat: prime message_file resources

feat: implement conversation access validation in chat route

refactor: remove file parameter from processFileUpload and use req.file instead

feat: add savedMessageIds set to track saved message IDs in BaseClient, to prevent unnecessary double-write to db

feat: prevent duplicate message saves by checking savedMessageIds in AgentController

refactor: skip legacy RAG API handling for agents

feat: add files field to convoSchema

refactor: update request type annotations from Express.Request to ServerRequest in file processing functions

feat: track conversation files

fix: resendFiles, addPreviousAttachments handling

feat: add ID validation for session_id and file_id in download route

feat: entity_id for code file uploads/downloads

fix: code file edge cases

feat: delete related tool calls

feat: add stream rate handling for LLM configuration

feat: enhance system content with attached file information

fix: improve error logging in resource priming function

* WIP: PoC, sequential agents

WIP: PoC Sequential Agents, first pass content data + bump agents package

fix: package-lock

WIP: PoC, o1 support, refactor bufferString

feat: convertJsonSchemaToZod

fix: form issues and schema defining erroneous model

fix: max length issue on agent form instructions, limit conversation messages to sequential agents

feat: add abort signal support to createRun function and AgentClient

feat: PoC, hide prior sequential agent steps

fix: update parameter naming from config to metadata in event handlers for clarity, add model to usage data

refactor: use only last contentData, track model for usage data

chore: bump agents package

fix: content parts issue

refactor: filter contentParts to include tool calls and relevant indices

feat: show function calls

refactor: filter context messages to exclude tool calls when no tools are available to the agent

fix: ensure tool call content is not undefined in formatMessages

feat: add agent_id field to conversationPreset schema

feat: hide sequential agents

feat: increase upload toast duration to 10 seconds

* refactor: tool context handling & update Code API Key Dialog

feat: toolContextMap

chore: skipSpecs -> useSpecs

ci: fix handleTools tests

feat: API Key Dialog

* feat: Agent Permissions Admin Controls

feat: replace label with button for prompt permission toggle

feat: update agent permissions

feat: enable experimental agents and streamline capability configuration

feat: implement access control for agents and enhance endpoint menu items

feat: add welcome message for agent selection in localization

feat: add agents permission to access control and update version to 0.7.57

* fix: update types in useAssistantListMap and useMentions hooks for better null handling

* feat: mention agents

* fix: agent tool resource race conditions when deleting agent tool resource files

* feat: add error handling for code execution with user feedback

* refactor: rename AdminControls to AdminSettings for clarity

* style: add gap to button in AdminSettings for improved layout

* refactor: separate agent query hooks and check access to enable fetching

* fix: remove unused provider from agent initialization options, creates issue with custom endpoints

* refactor: remove redundant/deprecated modelOptions from AgentClient processes

* chore: update @librechat/agents to version 1.8.5 in package.json and package-lock.json

* fix: minor styling issues + agent panel uniformity

* fix: agent edge cases when set endpoint is no longer defined

* refactor: remove unused cleanup function call from AppService

* fix: update link in ApiKeyDialog to point to pricing page

* fix: improve type handling and layout calculations in SidePanel component

* fix: add missing localization string for agent selection in SidePanel

* chore: form styling and localizations for upload filesearch/code interpreter

* fix: model selection placeholder logic in AgentConfig component

* style: agent capabilities

* fix: add localization for provider selection and improve dropdown styling in ModelPanel

* refactor: use gpt-4o-mini > gpt-3.5-turbo

* fix: agents configuration for loadDefaultInterface and update related tests

* feat: DALLE Agents support
  • Loading branch information
danny-avila authored Dec 4, 2024
1 parent affcebd commit 1a815f5
Show file tree
Hide file tree
Showing 189 changed files with 5,048 additions and 1,807 deletions.
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,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

Expand Down
45 changes: 40 additions & 5 deletions api/app/clients/BaseClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -84,7 +86,7 @@ class BaseClient {
return this.options.agent.id;
}

return this.modelOptions.model;
return this.modelOptions?.model ?? this.model;
}

/**
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
Expand All @@ -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,
},
});
Expand All @@ -574,6 +577,7 @@ class BaseClient {
const completion = await this.sendCompletion(payload, opts);
this.abortController.requestCompleted = true;

/** @type {TMessage} */
const responseMessage = {
messageId: responseMessageId,
conversationId,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand All @@ -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 },
});
Expand Down
6 changes: 3 additions & 3 deletions api/app/clients/OpenAIClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -982,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;
Expand Down
7 changes: 5 additions & 2 deletions api/app/clients/PluginsClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion api/app/clients/llm/createLLM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion api/app/clients/memory/summaryBuffer.demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
2 changes: 1 addition & 1 deletion api/app/clients/prompts/formatMessages.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ const formatAgentMessages = (payload) => {
new ToolMessage({
tool_call_id: tool_call.id,
name: tool_call.name,
content: output,
content: output || '',
}),
);
} else {
Expand Down
2 changes: 1 addition & 1 deletion api/app/clients/specs/BaseClient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('BaseClient', () => {
const options = {
// debug: true,
modelOptions: {
model: 'gpt-3.5-turbo',
model: 'gpt-4o-mini',
temperature: 0,
},
};
Expand Down
4 changes: 2 additions & 2 deletions api/app/clients/specs/OpenAIClient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
});

Expand Down
36 changes: 28 additions & 8 deletions api/app/clients/tools/structured/DALLE3.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -108,6 +110,19 @@ class DALLE3 extends Tool {
return `![generated image](${imageUrl})`;
}

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) {
Expand All @@ -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);
Expand All @@ -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,
});

Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,50 @@ const { logger } = require('~/config');
* @param {Object} options
* @param {ServerRequest} options.req
* @param {Agent['tool_resources']} options.tool_resources
* @returns
* @returns {Promise<{
* files: Array<{ file_id: string; filename: string }>,
* toolContext: string
* }>}
*/
const createFileSearchTool = async (options) => {
const { req, tool_resources } = options;
const primeFiles = async (options) => {
const { 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 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,
});
}

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}`;
return { files, toolContext };
};

const FileSearch = tool(
/**
*
* @param {Object} options
* @param {ServerRequest} options.req
* @param {Array<{ file_id: string; filename: string }>} options.files
* @returns
*/
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.';
Expand Down Expand Up @@ -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()
Expand All @@ -97,8 +127,6 @@ const createFileSearchTool = async (options) => {
}),
},
);

return FileSearch;
};

module.exports = createFileSearchTool;
module.exports = { createFileSearchTool, primeFiles };
Loading

0 comments on commit 1a815f5

Please sign in to comment.