diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index 77e14c07d..e24bffa18 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -5,6 +5,7 @@ const { isAgentsEndpoint, isParamEndpoint, EModelEndpoint, + ContentTypes, excludedKeys, ErrorTypes, Constants, @@ -1021,11 +1022,17 @@ class BaseClient { const processValue = (value) => { if (Array.isArray(value)) { for (let item of value) { - if (!item || !item.type || item.type === 'image_url') { + if ( + !item || + !item.type || + item.type === ContentTypes.THINK || + item.type === ContentTypes.ERROR || + item.type === ContentTypes.IMAGE_URL + ) { continue; } - if (item.type === 'tool_call' && item.tool_call != null) { + if (item.type === ContentTypes.TOOL_CALL && item.tool_call != null) { const toolName = item.tool_call?.name || ''; if (toolName != null && toolName && typeof toolName === 'string') { numTokens += this.getTokenCount(toolName); diff --git a/api/app/clients/prompts/formatMessages.js b/api/app/clients/prompts/formatMessages.js index 4e8d3bd5a..9fa0d4049 100644 --- a/api/app/clients/prompts/formatMessages.js +++ b/api/app/clients/prompts/formatMessages.js @@ -211,7 +211,7 @@ const formatAgentMessages = (payload) => { } else if (part.type === ContentTypes.THINK) { hasReasoning = true; continue; - } else if (part.type === ContentTypes.ERROR) { + } else if (part.type === ContentTypes.ERROR || part.type === ContentTypes.AGENT_UPDATE) { continue; } else { currentContent.push(part); diff --git a/api/app/clients/tools/structured/DALLE3.js b/api/app/clients/tools/structured/DALLE3.js index 81200e3a6..fc0f1851f 100644 --- a/api/app/clients/tools/structured/DALLE3.js +++ b/api/app/clients/tools/structured/DALLE3.js @@ -172,7 +172,7 @@ Error Message: ${error.message}`); { type: ContentTypes.IMAGE_URL, image_url: { - url: `data:image/jpeg;base64,${base64}`, + url: `data:image/png;base64,${base64}`, }, }, ]; diff --git a/api/models/Banner.js b/api/models/Banner.js index 0f20faeba..399a8e72e 100644 --- a/api/models/Banner.js +++ b/api/models/Banner.js @@ -28,4 +28,4 @@ const getBanner = async (user) => { } }; -module.exports = { getBanner }; +module.exports = { Banner, getBanner }; diff --git a/api/models/tx.js b/api/models/tx.js index b534e7edc..67301d0c4 100644 --- a/api/models/tx.js +++ b/api/models/tx.js @@ -61,6 +61,7 @@ const bedrockValues = { '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 }, + 'deepseek.r1': { prompt: 1.35, completion: 5.4 }, }; /** diff --git a/api/models/tx.spec.js b/api/models/tx.spec.js index b04eacc9f..f612e222b 100644 --- a/api/models/tx.spec.js +++ b/api/models/tx.spec.js @@ -288,7 +288,7 @@ describe('AWS Bedrock Model Tests', () => { }); describe('Deepseek Model Tests', () => { - const deepseekModels = ['deepseek-chat', 'deepseek-coder', 'deepseek-reasoner']; + const deepseekModels = ['deepseek-chat', 'deepseek-coder', 'deepseek-reasoner', 'deepseek.r1']; it('should return the correct prompt multipliers for all models', () => { const results = deepseekModels.map((model) => { diff --git a/api/package.json b/api/package.json index 9197691a7..ab2a82dbd 100644 --- a/api/package.json +++ b/api/package.json @@ -42,10 +42,10 @@ "@keyv/redis": "^2.8.1", "@langchain/community": "^0.3.34", "@langchain/core": "^0.3.40", - "@langchain/google-genai": "^0.1.9", - "@langchain/google-vertexai": "^0.2.0", + "@langchain/google-genai": "^0.1.11", + "@langchain/google-vertexai": "^0.2.2", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.2.0", + "@librechat/agents": "^2.2.8", "@librechat/data-schemas": "*", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.8.2", diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index d591fe324..4b995bb06 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -7,7 +7,16 @@ // validateVisionModel, // mapModelToAzureConfig, // } = require('librechat-data-provider'); -const { Callback, createMetadataAggregator } = require('@librechat/agents'); +require('events').EventEmitter.defaultMaxListeners = 100; +const { + Callback, + GraphEvents, + formatMessage, + formatAgentMessages, + formatContentStrings, + getTokenCountForMessage, + createMetadataAggregator, +} = require('@librechat/agents'); const { Constants, VisionModes, @@ -17,24 +26,19 @@ const { KnownEndpoints, anthropicSchema, isAgentsEndpoint, + AgentCapabilities, bedrockInputSchema, removeNullishValues, } = require('librechat-data-provider'); -const { - formatMessage, - addCacheControl, - formatAgentMessages, - formatContentStrings, - createContextHandlers, -} = require('~/app/clients/prompts'); +const { getCustomEndpointConfig, checkCapability } = require('~/server/services/Config'); +const { addCacheControl, createContextHandlers } = require('~/app/clients/prompts'); const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens'); const { getBufferString, HumanMessage } = require('@langchain/core/messages'); const { encodeAndFormat } = require('~/server/services/Files/images/encode'); -const { getCustomEndpointConfig } = require('~/server/services/Config'); const Tokenizer = require('~/server/services/Tokenizer'); const BaseClient = require('~/app/clients/BaseClient'); +const { logger, sendEvent } = require('~/config'); const { createRun } = require('./run'); -const { logger } = require('~/config'); /** @typedef {import('@librechat/agents').MessageContentComplex} MessageContentComplex */ /** @typedef {import('@langchain/core/runnables').RunnableConfig} RunnableConfig */ @@ -99,6 +103,8 @@ class AgentClient extends BaseClient { this.outputTokensKey = 'output_tokens'; /** @type {UsageMetadata} */ this.usage; + /** @type {Record} */ + this.indexTokenCountMap = {}; } /** @@ -377,6 +383,10 @@ class AgentClient extends BaseClient { })); } + for (let i = 0; i < messages.length; i++) { + this.indexTokenCountMap[i] = messages[i].tokenCount; + } + const result = { tokenCountMap, prompt: payload, @@ -622,6 +632,9 @@ class AgentClient extends BaseClient { // }); // } + /** @type {TCustomConfig['endpoints']['agents']} */ + const agentsEConfig = this.options.req.app.locals[EModelEndpoint.agents]; + /** @type {Partial & { version: 'v1' | 'v2'; run_id?: string; streamMode: string }} */ const config = { configurable: { @@ -629,19 +642,30 @@ class AgentClient extends BaseClient { last_agent_index: this.agentConfigs?.size ?? 0, hide_sequential_outputs: this.options.agent.hide_sequential_outputs, }, - recursionLimit: this.options.req.app.locals[EModelEndpoint.agents]?.recursionLimit, + recursionLimit: agentsEConfig?.recursionLimit, signal: abortController.signal, streamMode: 'values', version: 'v2', }; - const initialMessages = formatAgentMessages(payload); + const toolSet = new Set((this.options.agent.tools ?? []).map((tool) => tool && tool.name)); + let { messages: initialMessages, indexTokenCountMap } = formatAgentMessages( + payload, + this.indexTokenCountMap, + toolSet, + ); if (legacyContentEndpoints.has(this.options.agent.endpoint)) { - formatContentStrings(initialMessages); + initialMessages = formatContentStrings(initialMessages); } /** @type {ReturnType} */ let run; + const countTokens = ((text) => this.getTokenCount(text)).bind(this); + + /** @type {(message: BaseMessage) => number} */ + const tokenCounter = (message) => { + return getTokenCountForMessage(message, countTokens); + }; /** * @@ -649,12 +673,23 @@ class AgentClient extends BaseClient { * @param {BaseMessage[]} messages * @param {number} [i] * @param {TMessageContentParts[]} [contentData] + * @param {Record} [currentIndexCountMap] */ - const runAgent = async (agent, _messages, i = 0, contentData = []) => { + const runAgent = async (agent, _messages, i = 0, contentData = [], _currentIndexCountMap) => { config.configurable.model = agent.model_parameters.model; + const currentIndexCountMap = _currentIndexCountMap ?? indexTokenCountMap; if (i > 0) { this.model = agent.model_parameters.model; } + if (agent.recursion_limit && typeof agent.recursion_limit === 'number') { + config.recursionLimit = agent.recursion_limit; + } + if ( + agentsEConfig?.maxRecursionLimit && + config.recursionLimit > agentsEConfig?.maxRecursionLimit + ) { + config.recursionLimit = agentsEConfig?.maxRecursionLimit; + } config.configurable.agent_id = agent.id; config.configurable.name = agent.name; config.configurable.agent_index = i; @@ -717,11 +752,29 @@ class AgentClient extends BaseClient { } if (contentData.length) { + const agentUpdate = { + type: ContentTypes.AGENT_UPDATE, + [ContentTypes.AGENT_UPDATE]: { + index: contentData.length, + runId: this.responseMessageId, + agentId: agent.id, + }, + }; + const streamData = { + event: GraphEvents.ON_AGENT_UPDATE, + data: agentUpdate, + }; + this.options.aggregateContent(streamData); + sendEvent(this.options.res, streamData); + contentData.push(agentUpdate); run.Graph.contentData = contentData; } await run.processStream({ messages }, config, { keepContent: i !== 0, + tokenCounter, + indexTokenCountMap: currentIndexCountMap, + maxContextTokens: agent.maxContextTokens, callbacks: { [Callback.TOOL_ERROR]: (graph, error, toolId) => { logger.error( @@ -735,9 +788,13 @@ class AgentClient extends BaseClient { }; await runAgent(this.options.agent, initialMessages); - let finalContentStart = 0; - if (this.agentConfigs && this.agentConfigs.size > 0) { + if ( + this.agentConfigs && + this.agentConfigs.size > 0 && + (await checkCapability(this.options.req, AgentCapabilities.chain)) + ) { + const windowSize = 5; let latestMessage = initialMessages.pop().content; if (typeof latestMessage !== 'string') { latestMessage = latestMessage[0].text; @@ -745,7 +802,16 @@ class AgentClient extends BaseClient { let i = 1; let runMessages = []; - const lastFiveMessages = initialMessages.slice(-5); + const windowIndexCountMap = {}; + const windowMessages = initialMessages.slice(-windowSize); + let currentIndex = 4; + for (let i = initialMessages.length - 1; i >= 0; i--) { + windowIndexCountMap[currentIndex] = indexTokenCountMap[i]; + currentIndex--; + if (currentIndex < 0) { + break; + } + } for (const [agentId, agent] of this.agentConfigs) { if (abortController.signal.aborted === true) { break; @@ -780,7 +846,9 @@ class AgentClient extends BaseClient { } try { const contextMessages = []; - for (const message of lastFiveMessages) { + const runIndexCountMap = {}; + for (let i = 0; i < windowMessages.length; i++) { + const message = windowMessages[i]; const messageType = message._getType(); if ( (!agent.tools || agent.tools.length === 0) && @@ -788,11 +856,13 @@ class AgentClient extends BaseClient { ) { continue; } - + runIndexCountMap[contextMessages.length] = windowIndexCountMap[i]; contextMessages.push(message); } - const currentMessages = [...contextMessages, new HumanMessage(bufferString)]; - await runAgent(agent, currentMessages, i, contentData); + const bufferMessage = new HumanMessage(bufferString); + runIndexCountMap[contextMessages.length] = tokenCounter(bufferMessage); + const currentMessages = [...contextMessages, bufferMessage]; + await runAgent(agent, currentMessages, i, contentData, runIndexCountMap); } catch (err) { logger.error( `[api/server/controllers/agents/client.js #chatCompletion] Error running agent ${agentId} (${i})`, @@ -803,6 +873,7 @@ class AgentClient extends BaseClient { } } + /** Note: not implemented */ if (config.configurable.hide_sequential_outputs !== true) { finalContentStart = 0; } diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 08327ec61..731dee69a 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -1,10 +1,11 @@ const fs = require('fs').promises; const { nanoid } = require('nanoid'); const { - FileContext, - Constants, Tools, + Constants, + FileContext, SystemRoles, + EToolResources, actionDelimiter, } = require('librechat-data-provider'); const { @@ -203,14 +204,21 @@ const duplicateAgentHandler = async (req, res) => { } const { - _id: __id, id: _id, + _id: __id, author: _author, createdAt: _createdAt, updatedAt: _updatedAt, + tool_resources: _tool_resources = {}, ...cloneData } = agent; + if (_tool_resources?.[EToolResources.ocr]) { + cloneData.tool_resources = { + [EToolResources.ocr]: _tool_resources[EToolResources.ocr], + }; + } + const newAgentId = `agent_${nanoid()}`; const newAgentData = Object.assign(cloneData, { id: newAgentId, diff --git a/api/server/services/ActionService.js b/api/server/services/ActionService.js index 660e7aeb0..c332cdfcf 100644 --- a/api/server/services/ActionService.js +++ b/api/server/services/ActionService.js @@ -161,9 +161,9 @@ async function createActionTool({ if (metadata.auth && metadata.auth.type !== AuthTypeEnum.None) { try { - const action_id = action.action_id; - const identifier = `${req.user.id}:${action.action_id}`; if (metadata.auth.type === AuthTypeEnum.OAuth && metadata.auth.authorization_url) { + const action_id = action.action_id; + const identifier = `${req.user.id}:${action.action_id}`; const requestLogin = async () => { const { args: _args, stepId, ...toolCall } = config.toolCall ?? {}; if (!stepId) { diff --git a/api/server/services/Config/getEndpointsConfig.js b/api/server/services/Config/getEndpointsConfig.js index 4f8bde68a..016f5f744 100644 --- a/api/server/services/Config/getEndpointsConfig.js +++ b/api/server/services/Config/getEndpointsConfig.js @@ -72,4 +72,15 @@ async function getEndpointsConfig(req) { return endpointsConfig; } -module.exports = { getEndpointsConfig }; +/** + * @param {ServerRequest} req + * @param {import('librechat-data-provider').AgentCapabilities} capability + * @returns {Promise} + */ +const checkCapability = async (req, capability) => { + const endpointsConfig = await getEndpointsConfig(req); + const capabilities = endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []; + return capabilities.includes(capability); +}; + +module.exports = { getEndpointsConfig, checkCapability }; diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 11c8dc6fc..cb539d3bc 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -204,8 +204,7 @@ const initializeAgentOptions = async ({ toolContextMap, maxContextTokens: agent.max_context_tokens ?? - getModelMaxTokens(tokensModel, providerEndpointMap[provider]) ?? - 4000, + (getModelMaxTokens(tokensModel, providerEndpointMap[provider]) ?? 4000) * 0.9, }; }; @@ -275,11 +274,13 @@ const initializeClient = async ({ req, res, endpointOption }) => { const client = new AgentClient({ req, + res, sender, contentParts, agentConfigs, eventHandlers, collectedUsage, + aggregateContent, artifactPromises, agent: primaryConfig, spec: endpointOption.spec, diff --git a/api/server/services/Endpoints/bedrock/initialize.js b/api/server/services/Endpoints/bedrock/initialize.js index 51d004056..4d9ba361c 100644 --- a/api/server/services/Endpoints/bedrock/initialize.js +++ b/api/server/services/Endpoints/bedrock/initialize.js @@ -55,6 +55,7 @@ const initializeClient = async ({ req, res, endpointOption }) => { const client = new AgentClient({ req, + res, agent, sender, // tools, diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index 1bfadc4b2..78a4976e2 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -29,7 +29,7 @@ const { addAgentResourceFile, removeAgentResourceFiles } = require('~/models/Age const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { createFile, updateFileUsage, deleteFiles } = require('~/models/File'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); -const { getEndpointsConfig } = require('~/server/services/Config'); +const { checkCapability } = require('~/server/services/Config'); const { LB_QueueAsyncCall } = require('~/server/utils/queue'); const { getStrategyFunctions } = require('./strategies'); const { determineFileType } = require('~/server/utils'); @@ -457,17 +457,6 @@ const processFileUpload = async ({ req, res, metadata }) => { res.status(200).json({ message: 'File uploaded and processed successfully', ...result }); }; -/** - * @param {ServerRequest} req - * @param {AgentCapabilities} capability - * @returns {Promise} - */ -const checkCapability = async (req, capability) => { - const endpointsConfig = await getEndpointsConfig(req); - const capabilities = endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []; - return capabilities.includes(capability); -}; - /** * Applies the current strategy for file uploads. * Saves file metadata to the database with an expiry TTL. diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index f3e4efb6e..969ca8d8f 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -362,7 +362,12 @@ async function processRequiredActions(client, requiredActions) { continue; } - tool = await createActionTool({ action: actionSet, requestBuilder }); + tool = await createActionTool({ + req: client.req, + res: client.res, + 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}`, diff --git a/api/server/utils/handleText.js b/api/server/utils/handleText.js index 1b9cda28d..f593d6c86 100644 --- a/api/server/utils/handleText.js +++ b/api/server/utils/handleText.js @@ -204,6 +204,7 @@ function generateConfig(key, baseURL, endpoint) { AgentCapabilities.actions, AgentCapabilities.tools, AgentCapabilities.ocr, + AgentCapabilities.chain, ]; } diff --git a/api/utils/tokens.js b/api/utils/tokens.js index 8edfb0a31..58aaf7051 100644 --- a/api/utils/tokens.js +++ b/api/utils/tokens.js @@ -92,6 +92,7 @@ const anthropicModels = { const deepseekModels = { 'deepseek-reasoner': 63000, // -1000 from max (API) deepseek: 63000, // -1000 from max (API) + 'deepseek.r1': 127500, }; const metaModels = { diff --git a/api/utils/tokens.spec.js b/api/utils/tokens.spec.js index d4dbb3049..e5ae21b64 100644 --- a/api/utils/tokens.spec.js +++ b/api/utils/tokens.spec.js @@ -423,6 +423,9 @@ describe('Meta Models Tests', () => { expect(getModelMaxTokens('deepseek-reasoner')).toBe( maxTokensMap[EModelEndpoint.openAI]['deepseek-reasoner'], ); + expect(getModelMaxTokens('deepseek.r1')).toBe( + maxTokensMap[EModelEndpoint.openAI]['deepseek.r1'], + ); }); }); diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts index f4b8aac9f..982cbfdb1 100644 --- a/client/src/common/agents-types.ts +++ b/client/src/common/agents-types.ts @@ -28,4 +28,5 @@ export type AgentForm = { provider?: AgentProvider | OptionWithIcon; agent_ids?: string[]; [AgentCapabilities.artifacts]?: ArtifactModes | string; + recursion_limit?: number; } & TAgentCapabilities; diff --git a/client/src/common/types.ts b/client/src/common/types.ts index f0b2b8a23..975f46893 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -131,6 +131,7 @@ export interface DataColumnMeta { } export enum Panel { + advanced = 'advanced', builder = 'builder', actions = 'actions', model = 'model', @@ -181,6 +182,7 @@ export type AgentPanelProps = { activePanel?: string; action?: t.Action; actions?: t.Action[]; + createMutation: UseMutationResult; setActivePanel: React.Dispatch>; setAction: React.Dispatch>; endpointsConfig?: t.TEndpointsConfig; diff --git a/client/src/components/Chat/Messages/Content/ContentParts.tsx b/client/src/components/Chat/Messages/Content/ContentParts.tsx index ddf08976e..3805e0bb4 100644 --- a/client/src/components/Chat/Messages/Content/ContentParts.tsx +++ b/client/src/components/Chat/Messages/Content/ContentParts.tsx @@ -139,6 +139,7 @@ const ContentParts = memo( isSubmitting={isSubmitting} key={`part-${messageId}-${idx}`} isCreatedByUser={isCreatedByUser} + isLast={idx === content.length - 1} showCursor={idx === content.length - 1 && isLast} /> diff --git a/client/src/components/Chat/Messages/Content/Markdown.tsx b/client/src/components/Chat/Messages/Content/Markdown.tsx index e01de091c..ee134b0e5 100644 --- a/client/src/components/Chat/Messages/Content/Markdown.tsx +++ b/client/src/components/Chat/Messages/Content/Markdown.tsx @@ -166,15 +166,12 @@ export const p: React.ElementType = memo(({ children }: TParagraphProps) => { return

{children}

; }); -const cursor = ' '; - type TContentProps = { content: string; - showCursor?: boolean; isLatestMessage: boolean; }; -const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentProps) => { +const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => { const LaTeXParsing = useRecoilValue(store.LaTeXParsing); const isInitializing = content === ''; @@ -240,7 +237,7 @@ const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentPr } } > - {isLatestMessage && (showCursor ?? false) ? currentContent + cursor : currentContent} + {currentContent} diff --git a/client/src/components/Chat/Messages/Content/MessageContent.tsx b/client/src/components/Chat/Messages/Content/MessageContent.tsx index 1547a01d8..f70a15b77 100644 --- a/client/src/components/Chat/Messages/Content/MessageContent.tsx +++ b/client/src/components/Chat/Messages/Content/MessageContent.tsx @@ -83,9 +83,7 @@ const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplay let content: React.ReactElement; if (!isCreatedByUser) { - content = ( - - ); + content = ; } else if (enableUserMsgMarkdown) { content = ; } else { diff --git a/client/src/components/Chat/Messages/Content/Part.tsx b/client/src/components/Chat/Messages/Content/Part.tsx index 2430bee6f..1351efd59 100644 --- a/client/src/components/Chat/Messages/Content/Part.tsx +++ b/client/src/components/Chat/Messages/Content/Part.tsx @@ -8,9 +8,11 @@ import { import { memo } from 'react'; import type { TMessageContentParts, TAttachment } from 'librechat-data-provider'; import { ErrorMessage } from './MessageContent'; +import AgentUpdate from './Parts/AgentUpdate'; import ExecuteCode from './Parts/ExecuteCode'; import RetrievalCall from './RetrievalCall'; import Reasoning from './Parts/Reasoning'; +import EmptyText from './Parts/EmptyText'; import CodeAnalyze from './CodeAnalyze'; import Container from './Container'; import ToolCall from './ToolCall'; @@ -20,145 +22,159 @@ import Image from './Image'; type PartProps = { part?: TMessageContentParts; + isLast?: boolean; isSubmitting: boolean; showCursor: boolean; isCreatedByUser: boolean; attachments?: TAttachment[]; }; -const Part = memo(({ part, isSubmitting, attachments, showCursor, isCreatedByUser }: PartProps) => { - if (!part) { - return null; - } - - if (part.type === ContentTypes.ERROR) { - return ( - - ); - } else if (part.type === ContentTypes.TEXT) { - const text = typeof part.text === 'string' ? part.text : part.text.value; - - if (typeof text !== 'string') { - return null; - } - if (part.tool_call_ids != null && !text) { - return null; - } - return ( - - - - ); - } else if (part.type === ContentTypes.THINK) { - const reasoning = typeof part.think === 'string' ? part.think : part.think.value; - if (typeof reasoning !== 'string') { - return null; - } - return ; - } else if (part.type === ContentTypes.TOOL_CALL) { - const toolCall = part[ContentTypes.TOOL_CALL]; - - if (!toolCall) { +const Part = memo( + ({ part, isSubmitting, attachments, isLast, showCursor, isCreatedByUser }: PartProps) => { + if (!part) { return null; } - const isToolCall = - 'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL); - if (isToolCall && toolCall.name === Tools.execute_code) { + if (part.type === ContentTypes.ERROR) { return ( - ); - } else if (isToolCall) { + } else if (part.type === ContentTypes.AGENT_UPDATE) { return ( - - ); - } else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) { - const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER]; - return ( - - ); - } else if ( - toolCall.type === ToolCallTypes.RETRIEVAL || - toolCall.type === ToolCallTypes.FILE_SEARCH - ) { - return ( - - ); - } else if ( - toolCall.type === ToolCallTypes.FUNCTION && - ToolCallTypes.FUNCTION in toolCall && - imageGenTools.has(toolCall.function.name) - ) { - return ( - - ); - } else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) { - if (isImageVisionTool(toolCall)) { - if (isSubmitting && showCursor) { - return ( + <> + + {isLast && showCursor && ( - + - ); - } + )} + + ); + } else if (part.type === ContentTypes.TEXT) { + const text = typeof part.text === 'string' ? part.text : part.text.value; + + if (typeof text !== 'string') { + return null; + } + if (part.tool_call_ids != null && !text) { + return null; + } + return ( + + + + ); + } else if (part.type === ContentTypes.THINK) { + const reasoning = typeof part.think === 'string' ? part.think : part.think.value; + if (typeof reasoning !== 'string') { + return null; + } + return ; + } else if (part.type === ContentTypes.TOOL_CALL) { + const toolCall = part[ContentTypes.TOOL_CALL]; + + if (!toolCall) { return null; } + const isToolCall = + 'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL); + if (isToolCall && toolCall.name === Tools.execute_code) { + return ( + + ); + } else if (isToolCall) { + return ( + + ); + } else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) { + const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER]; + return ( + + ); + } else if ( + toolCall.type === ToolCallTypes.RETRIEVAL || + toolCall.type === ToolCallTypes.FILE_SEARCH + ) { + return ( + + ); + } else if ( + toolCall.type === ToolCallTypes.FUNCTION && + ToolCallTypes.FUNCTION in toolCall && + imageGenTools.has(toolCall.function.name) + ) { + return ( + + ); + } else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) { + if (isImageVisionTool(toolCall)) { + if (isSubmitting && showCursor) { + return ( + + + + ); + } + return null; + } + + return ( + + ); + } + } else if (part.type === ContentTypes.IMAGE_FILE) { + const imageFile = part[ContentTypes.IMAGE_FILE]; + const height = imageFile.height ?? 1920; + const width = imageFile.width ?? 1080; return ( - ); } - } else if (part.type === ContentTypes.IMAGE_FILE) { - const imageFile = part[ContentTypes.IMAGE_FILE]; - const height = imageFile.height ?? 1920; - const width = imageFile.width ?? 1080; - return ( - - ); - } - return null; -}); + return null; + }, +); export default Part; diff --git a/client/src/components/Chat/Messages/Content/Parts/AgentUpdate.tsx b/client/src/components/Chat/Messages/Content/Parts/AgentUpdate.tsx new file mode 100644 index 000000000..4dca00107 --- /dev/null +++ b/client/src/components/Chat/Messages/Content/Parts/AgentUpdate.tsx @@ -0,0 +1,39 @@ +import React, { useMemo } from 'react'; +import { EModelEndpoint } from 'librechat-data-provider'; +import { useAgentsMapContext } from '~/Providers'; +import Icon from '~/components/Endpoints/Icon'; + +interface AgentUpdateProps { + currentAgentId: string; +} + +const AgentUpdate: React.FC = ({ currentAgentId }) => { + const agentsMap = useAgentsMapContext() || {}; + const currentAgent = useMemo(() => agentsMap[currentAgentId], [agentsMap, currentAgentId]); + if (!currentAgentId) { + return null; + } + return ( +
+
+
+
+
+
+
+
+
+ +
+
{currentAgent?.name}
+
+
+ ); +}; + +export default AgentUpdate; diff --git a/client/src/components/Chat/Messages/Content/Parts/EmptyText.tsx b/client/src/components/Chat/Messages/Content/Parts/EmptyText.tsx new file mode 100644 index 000000000..1b514164d --- /dev/null +++ b/client/src/components/Chat/Messages/Content/Parts/EmptyText.tsx @@ -0,0 +1,17 @@ +import { memo } from 'react'; + +const EmptyTextPart = memo(() => { + return ( +
+
+
+

+ +

+
+
+
+ ); +}); + +export default EmptyTextPart; diff --git a/client/src/components/Chat/Messages/Content/Parts/Text.tsx b/client/src/components/Chat/Messages/Content/Parts/Text.tsx index 7c207f151..d4a605aea 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Text.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Text.tsx @@ -29,9 +29,7 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => const content: ContentType = useMemo(() => { if (!isCreatedByUser) { - return ( - - ); + return ; } else if (enableUserMsgMarkdown) { return ; } else { diff --git a/client/src/components/SidePanel/Agents/AdminSettings.tsx b/client/src/components/SidePanel/Agents/AdminSettings.tsx index 6ca21d131..5fb13fd04 100644 --- a/client/src/components/SidePanel/Agents/AdminSettings.tsx +++ b/client/src/components/SidePanel/Agents/AdminSettings.tsx @@ -142,7 +142,7 @@ const AdminSettings = () => { + ); +}; + +export default AdvancedButton; diff --git a/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx b/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx new file mode 100644 index 000000000..0ead79cd3 --- /dev/null +++ b/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx @@ -0,0 +1,55 @@ +import { useMemo } from 'react'; +import { ChevronLeft } from 'lucide-react'; +import { AgentCapabilities } from 'librechat-data-provider'; +import { useFormContext, Controller } from 'react-hook-form'; +import type { AgentForm, AgentPanelProps } from '~/common'; +import MaxAgentSteps from './MaxAgentSteps'; +import AgentChain from './AgentChain'; +import { useLocalize } from '~/hooks'; +import { Panel } from '~/common'; + +export default function AdvancedPanel({ + agentsConfig, + setActivePanel, +}: Pick) { + const localize = useLocalize(); + const methods = useFormContext(); + const { control, watch } = methods; + const currentAgentId = watch('id'); + const chainEnabled = useMemo( + () => agentsConfig?.capabilities.includes(AgentCapabilities.chain) ?? false, + [agentsConfig], + ); + + return ( +
+
+
+ +
+
{localize('com_ui_advanced_settings')}
+
+
+ + {chainEnabled && ( + } + /> + )} +
+
+ ); +} diff --git a/client/src/components/SidePanel/Agents/Advanced/AgentChain.tsx b/client/src/components/SidePanel/Agents/Advanced/AgentChain.tsx new file mode 100644 index 000000000..138092711 --- /dev/null +++ b/client/src/components/SidePanel/Agents/Advanced/AgentChain.tsx @@ -0,0 +1,179 @@ +import { X, Link2, PlusCircle } from 'lucide-react'; +import { EModelEndpoint } from 'librechat-data-provider'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; +import type { ControllerRenderProps } from 'react-hook-form'; +import type { AgentForm, OptionWithIcon } from '~/common'; +import ControlCombobox from '~/components/ui/ControlCombobox'; +import { HoverCard, HoverCardPortal, HoverCardContent, HoverCardTrigger } from '~/components/ui'; +import { CircleHelpIcon } from '~/components/svg'; +import { useAgentsMapContext } from '~/Providers'; +import Icon from '~/components/Endpoints/Icon'; +import { useLocalize } from '~/hooks'; +import { ESide } from '~/common'; + +interface AgentChainProps { + field: ControllerRenderProps; + currentAgentId: string; +} + +/** TODO: make configurable */ +const MAX_AGENTS = 10; + +const AgentChain: React.FC = ({ field, currentAgentId }) => { + const localize = useLocalize(); + const [newAgentId, setNewAgentId] = useState(''); + const agentsMap = useAgentsMapContext() || {}; + const agentIds = field.value || []; + + const agents = useMemo(() => Object.values(agentsMap), [agentsMap]); + + const selectableAgents = useMemo( + () => + agents + .filter((agent) => agent?.id !== currentAgentId) + .map( + (agent) => + ({ + label: agent?.name || '', + value: agent?.id, + icon: ( + + ), + }) as OptionWithIcon, + ), + [agents, currentAgentId], + ); + + const getAgentDetails = useCallback((id: string) => agentsMap[id], [agentsMap]); + + useEffect(() => { + if (newAgentId && agentIds.length < MAX_AGENTS) { + field.onChange([...agentIds, newAgentId]); + setNewAgentId(''); + } + }, [newAgentId, agentIds, field]); + + const removeAgentAt = (index: number) => { + field.onChange(agentIds.filter((_, i) => i !== index)); + }; + + const updateAgentAt = (index: number, id: string) => { + const updated = [...agentIds]; + updated[index] = id; + field.onChange(updated); + }; + + return ( + +
+
+ + + + +
+
+ {agentIds.length} / {MAX_AGENTS} +
+
+
+ {/* Current fixed agent */} +
+
+
+ +
+
+ {getAgentDetails(currentAgentId)?.name} +
+
+
+ {} + {agentIds.map((agentId, idx) => ( + +
+ updateAgentAt(idx, id)} + selectPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_select') })} + searchPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_search') })} + items={selectableAgents} + displayValue={getAgentDetails(agentId)?.name ?? ''} + SelectIcon={ + + } + className="flex-1 border-border-heavy" + containerClassName="px-0" + /> + {/* Future Settings button? */} + {/* */} + +
+ {idx < agentIds.length - 1 && ( + + )} +
+ ))} + + {agentIds.length < MAX_AGENTS && ( + <> + {agentIds.length > 0 && } + } + /> + + )} + + {agentIds.length >= MAX_AGENTS && ( +

+ {localize('com_ui_agent_chain_max', { 0: MAX_AGENTS })} +

+ )} +
+ + +
+

{localize('com_ui_agent_chain_info')}

+
+
+
+
+ ); +}; + +export default AgentChain; diff --git a/client/src/components/SidePanel/Agents/Advanced/MaxAgentSteps.tsx b/client/src/components/SidePanel/Agents/Advanced/MaxAgentSteps.tsx new file mode 100644 index 000000000..5e334282f --- /dev/null +++ b/client/src/components/SidePanel/Agents/Advanced/MaxAgentSteps.tsx @@ -0,0 +1,52 @@ +import { useFormContext, Controller } from 'react-hook-form'; +import type { AgentForm } from '~/common'; +import { + HoverCard, + FormInput, + HoverCardPortal, + HoverCardContent, + HoverCardTrigger, +} from '~/components/ui'; +import { CircleHelpIcon } from '~/components/svg'; +import { useLocalize } from '~/hooks'; +import { ESide } from '~/common'; + +export default function AdvancedPanel() { + const localize = useLocalize(); + const methods = useFormContext(); + const { control } = methods; + + return ( + + ( + + + + } + /> + )} + /> + + +
+

+ {localize('com_ui_agent_recursion_limit_info')} +

+
+
+
+
+ ); +} diff --git a/client/src/components/SidePanel/Agents/AgentConfig.tsx b/client/src/components/SidePanel/Agents/AgentConfig.tsx index 7ece82bed..864ecd817 100644 --- a/client/src/components/SidePanel/Agents/AgentConfig.tsx +++ b/client/src/components/SidePanel/Agents/AgentConfig.tsx @@ -1,32 +1,19 @@ import React, { useState, useMemo, useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { Controller, useWatch, useFormContext } from 'react-hook-form'; -import { - QueryKeys, - SystemRoles, - Permissions, - EModelEndpoint, - PermissionTypes, - AgentCapabilities, -} from 'librechat-data-provider'; +import { QueryKeys, EModelEndpoint, AgentCapabilities } from 'librechat-data-provider'; import type { TPlugin } from 'librechat-data-provider'; import type { AgentForm, AgentPanelProps, IconComponentTypes } from '~/common'; import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils'; -import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider'; -import { useLocalize, useAuthContext, useHasAccess } from '~/hooks'; import { useToastContext, useFileMapContext } from '~/Providers'; import { icons } from '~/components/Chat/Menus/Endpoints/Icons'; import Action from '~/components/SidePanel/Builder/Action'; import { ToolSelectDialog } from '~/components/Tools'; -import DuplicateAgent from './DuplicateAgent'; import { processAgentOption } from '~/utils'; -import AdminSettings from './AdminSettings'; -import DeleteButton from './DeleteButton'; import AgentAvatar from './AgentAvatar'; import FileContext from './FileContext'; -import { Spinner } from '~/components'; +import { useLocalize } from '~/hooks'; import FileSearch from './FileSearch'; -import ShareAgent from './ShareAgent'; import Artifacts from './Artifacts'; import AgentTool from './AgentTool'; import CodeForm from './Code/Form'; @@ -43,11 +30,10 @@ export default function AgentConfig({ setAction, actions = [], agentsConfig, - endpointsConfig, + createMutation, setActivePanel, - setCurrentAgentId, + endpointsConfig, }: AgentPanelProps) { - const { user } = useAuthContext(); const fileMap = useFileMapContext(); const queryClient = useQueryClient(); @@ -66,11 +52,6 @@ export default function AgentConfig({ const tools = useWatch({ control, name: 'tools' }); const agent_id = useWatch({ control, name: 'id' }); - const hasAccessToShareAgents = useHasAccess({ - permissionType: PermissionTypes.AGENTS, - permission: Permissions.SHARED_GLOBAL, - }); - const toolsEnabled = useMemo( () => agentsConfig?.capabilities.includes(AgentCapabilities.tools), [agentsConfig], @@ -156,46 +137,6 @@ export default function AgentConfig({ return _agent.code_files ?? []; }, [agent, agent_id, fileMap]); - /* Mutations */ - const update = useUpdateAgentMutation({ - onSuccess: (data) => { - showToast({ - message: `${localize('com_assistants_update_success')} ${ - data.name ?? localize('com_ui_agent') - }`, - }); - }, - onError: (err) => { - const error = err as Error; - showToast({ - message: `${localize('com_agents_update_error')}${ - error.message ? ` ${localize('com_ui_error')}: ${error.message}` : '' - }`, - status: 'error', - }); - }, - }); - - const create = useCreateAgentMutation({ - onSuccess: (data) => { - setCurrentAgentId(data.id); - showToast({ - message: `${localize('com_assistants_create_success')} ${ - data.name ?? localize('com_ui_agent') - }`, - }); - }, - onError: (err) => { - const error = err as Error; - showToast({ - message: `${localize('com_agents_create_error')}${ - error.message ? ` ${localize('com_ui_error')}: ${error.message}` : '' - }`, - status: 'error', - }); - }, - }); - const handleAddActions = useCallback(() => { if (!agent_id) { showToast({ @@ -225,26 +166,14 @@ export default function AgentConfig({ Icon = icons[iconKey]; } - const renderSaveButton = () => { - if (create.isLoading || update.isLoading) { - return