From ad74350036d186ebb17b74809434728ada8b2cac Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 30 Sep 2024 17:17:57 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20chore:=20merge=20latest=20dev=20?= =?UTF-8?q?build=20(#4288)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: agent initialization, add `collectedUsage` handling * style: improve side panel styling * refactor(loadAgent): Optimize order agent project ID retrieval * feat: code execution * fix: typing issues * feat: ExecuteCode content part * refactor: use local state for default collapsed state of analysis content parts * fix: code parsing in ExecuteCode component * chore: bump agents package, export loadAuthValues * refactor: Update handleTools.js to use EnvVar for code execution tool authentication * WIP * feat: download code outputs * fix(useEventHandlers): type issues * feat: backend handling for code outputs * Refactor: Remove console.log statement in Part.tsx * refactor: add attachments to TMessage/messageSchema * WIP: prelim handling for code outputs * feat: attachments rendering * refactor: improve attachments rendering * fix: attachments, nullish edge case, handle attachments from event stream, bump agents package * fix filename download * fix: tool assignment for 'run code' on agent creation * fix: image handling by adding attachments * refactor: prevent agent creation without provider/model * refactor: remove unnecessary space in agent creation success message * refactor: select first model if selecting provider from empty on form * fix: Agent avatar bug * fix: `defaultAgentFormValues` causing boolean typing issue and typeerror * fix: capabilities counting as tools, causing duplication of them * fix: formatted messages edge case where consecutive content text type parts with the latter having tool_call_ids would cause consecutive AI messages to be created. furthermore, content could not be an array for tool_use messages (anthropic limitation) * chore: bump @librechat/agents dependency to version 1.6.9 * feat: bedrock agents * feat: new Agents icon * feat: agent titling * feat: agent landing * refactor: allow sharing agent globally only if user is admin or author * feat: initial AgentPanelSkeleton * feat: AgentPanelSkeleton * feat: collaborative agents * chore: add potential authorName as part of schema * chore: Remove unnecessary console.log statement * WIP: agent model parameters * chore: ToolsDialog typing and tool related localization chnages * refactor: update tool instance type (latest langchain class), and rename google tool to 'google' proper * chore: add back tools * feat: Agent knowledge files upload * refactor: better verbiage for disabled knowledge * chore: debug logs for file deletions * chore: debug logs for file deletions * feat: upload/delete agent knowledge/file-search files * feat: file search UI for agents * feat: first pass, file search tool * chore: update default agent capabilities and info --- api/app/clients/BaseClient.js | 6 + .../prompts/formatAgentMessages.spec.js | 285 +++++++++++++ api/app/clients/prompts/formatMessages.js | 16 +- api/app/clients/tools/index.js | 4 +- .../clients/tools/structured/AzureAISearch.js | 4 +- api/app/clients/tools/structured/DALLE3.js | 2 +- .../clients/tools/structured/GoogleSearch.js | 3 +- .../tools/structured/StableDiffusion.js | 4 +- api/app/clients/tools/structured/Wolfram.js | 4 +- .../tools/util/createFileSearchTool.js | 104 +++++ api/app/clients/tools/util/handleTools.js | 95 +++-- api/app/clients/tools/util/index.js | 3 +- api/models/Agent.js | 169 +++++++- api/models/schema/agent.js | 21 +- api/models/schema/messageSchema.js | 23 + api/package.json | 2 +- api/server/controllers/agents/callbacks.js | 70 +++- api/server/controllers/agents/client.js | 17 +- api/server/controllers/agents/run.js | 4 +- api/server/controllers/agents/v1.js | 56 ++- api/server/controllers/assistants/v2.js | 1 - api/server/routes/agents/chat.js | 3 +- api/server/routes/agents/v1.js | 5 +- api/server/routes/bedrock/chat.js | 2 +- api/server/routes/files/files.js | 45 +- api/server/services/Config/EndpointService.js | 6 +- api/server/services/Endpoints/agents/build.js | 9 +- .../services/Endpoints/agents/initialize.js | 47 ++- .../Endpoints/{bedrock => agents}/title.js | 2 +- api/server/services/Files/Code/crud.js | 34 ++ api/server/services/Files/Code/index.js | 5 + api/server/services/Files/Code/process.js | 87 ++++ api/server/services/Files/process.js | 103 +++++ api/server/services/Files/strategies.js | 28 ++ api/server/services/ToolService.js | 15 +- api/server/utils/handleText.js | 20 +- api/typedefs.js | 19 + client/src/common/agents-types.ts | 13 +- client/src/common/types.ts | 5 +- .../components/Chat/Input/Files/FileRow.tsx | 25 +- client/src/components/Chat/Landing.tsx | 77 ++-- .../components/Chat/Menus/Endpoints/Icons.tsx | 18 +- .../Chat/Menus/Endpoints/MenuItem.tsx | 2 +- .../components/Chat/Menus/EndpointsMenu.tsx | 29 +- .../Chat/Messages/Content/CodeAnalyze.tsx | 101 +---- .../Chat/Messages/Content/ContentParts.tsx | 42 +- .../components/Chat/Messages/Content/Part.tsx | 232 +++++----- .../Messages/Content/Parts/CodeProgress.tsx | 90 ++++ .../Messages/Content/Parts/ExecuteCode.tsx | 127 ++++++ .../Messages/Content/Parts/LogContent.tsx | 66 +++ .../Chat/Messages/Content/Parts/LogLink.tsx | 52 +++ .../Chat/Messages/Content/ToolCall.tsx | 69 +-- client/src/components/Endpoints/ConvoIcon.tsx | 75 ++-- .../src/components/Endpoints/ConvoIconURL.tsx | 6 +- .../Endpoints/MessageEndpointIcon.tsx | 4 +- .../src/components/Endpoints/MinimalIcon.tsx | 23 +- .../src/components/Messages/ContentRender.tsx | 1 + .../SidePanel/Agents/AgentAvatar.tsx | 46 +- .../SidePanel/Agents/AgentConfig.tsx | 75 +++- .../SidePanel/Agents/AgentPanel.tsx | 85 +++- .../SidePanel/Agents/AgentPanelSkeleton.tsx | 70 ++++ .../SidePanel/Agents/AgentPanelSwitch.tsx | 17 +- .../SidePanel/Agents/AgentSelect.tsx | 43 +- .../SidePanel/Agents/CapabilitiesForm.tsx | 36 +- .../src/components/SidePanel/Agents/Code.tsx | 30 +- .../SidePanel/Agents/FileSearch.tsx | 120 ++++++ .../SidePanel/Agents/FileSearchCheckbox.tsx | 70 ++++ .../SidePanel/Agents/ModelPanel.tsx | 396 +++++++----------- .../SidePanel/Agents/ShareAgent.tsx | 109 ++++- .../SidePanel/Builder/AssistantPanel.tsx | 2 +- client/src/components/SidePanel/Nav.tsx | 10 +- .../SidePanel/Parameters/settings.ts | 9 + client/src/components/SidePanel/Switcher.tsx | 22 +- .../src/components/Tools/ToolSelectDialog.tsx | 17 +- client/src/components/svg/Sparkles.tsx | 6 +- client/src/data-provider/Files/index.ts | 1 + client/src/data-provider/Files/queries.ts | 84 ++++ client/src/data-provider/index.ts | 1 + client/src/data-provider/mutations.ts | 74 +++- client/src/data-provider/queries.ts | 68 +-- client/src/hooks/Agents/useSelectAgent.ts | 2 +- client/src/hooks/Files/useFileDeletion.ts | 63 ++- client/src/hooks/Files/useFileHandling.ts | 60 +-- client/src/hooks/Files/useUpdateFiles.ts | 10 +- client/src/hooks/SSE/index.ts | 1 + client/src/hooks/SSE/useAttachmentHandler.ts | 20 + client/src/hooks/SSE/useEventHandlers.ts | 52 ++- client/src/hooks/SSE/useSSE.ts | 14 +- client/src/localization/languages/Ar.ts | 4 +- client/src/localization/languages/Br.ts | 316 +++++++++----- client/src/localization/languages/De.ts | 4 +- client/src/localization/languages/Eng.ts | 19 +- client/src/localization/languages/Es.ts | 4 +- client/src/localization/languages/Fi.ts | 2 +- client/src/localization/languages/Fr.ts | 4 +- client/src/localization/languages/It.ts | 4 +- client/src/localization/languages/Jp.ts | 82 ++-- client/src/localization/languages/Ko.ts | 4 +- client/src/localization/languages/Ru.ts | 4 +- client/src/localization/languages/Tr.ts | 2 +- client/src/localization/languages/Zh.ts | 89 ++-- .../localization/languages/ZhTraditional.ts | 4 +- .../localization/prompts/instructions/Br.md | 2 +- .../localization/prompts/instructions/De.md | 2 +- .../localization/prompts/instructions/Es.md | 2 +- .../localization/prompts/instructions/It.md | 2 +- .../localization/prompts/instructions/Jp.md | 2 +- .../localization/prompts/instructions/Zh.md | 2 +- client/src/store/conversation.ts | 8 +- client/src/store/settings.ts | 2 +- client/src/utils/endpoints.ts | 81 ++-- client/src/utils/forms.tsx | 77 +++- client/src/utils/map.ts | 25 +- package-lock.json | 305 ++++++-------- packages/data-provider/package.json | 2 +- packages/data-provider/src/config.ts | 9 + packages/data-provider/src/data-service.ts | 22 +- packages/data-provider/src/file-config.ts | 1 + packages/data-provider/src/generate.ts | 2 +- packages/data-provider/src/roles.ts | 3 + packages/data-provider/src/schemas.ts | 14 +- .../data-provider/src/types/assistants.ts | 50 ++- packages/data-provider/src/types/files.ts | 4 + 123 files changed, 3611 insertions(+), 1541 deletions(-) create mode 100644 api/app/clients/prompts/formatAgentMessages.spec.js create mode 100644 api/app/clients/tools/util/createFileSearchTool.js rename api/server/services/Endpoints/{bedrock => agents}/title.js (93%) create mode 100644 api/server/services/Files/Code/crud.js create mode 100644 api/server/services/Files/Code/index.js create mode 100644 api/server/services/Files/Code/process.js create mode 100644 client/src/components/Chat/Messages/Content/Parts/CodeProgress.tsx create mode 100644 client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx create mode 100644 client/src/components/Chat/Messages/Content/Parts/LogContent.tsx create mode 100644 client/src/components/Chat/Messages/Content/Parts/LogLink.tsx create mode 100644 client/src/components/SidePanel/Agents/AgentPanelSkeleton.tsx create mode 100644 client/src/components/SidePanel/Agents/FileSearch.tsx create mode 100644 client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx create mode 100644 client/src/data-provider/Files/index.ts create mode 100644 client/src/data-provider/Files/queries.ts create mode 100644 client/src/hooks/SSE/useAttachmentHandler.ts diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index 51d75d063b..33e3df3ac6 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -42,6 +42,8 @@ class BaseClient { this.conversationId; /** @type {string} */ this.responseMessageId; + /** @type {TAttachment[]} */ + this.attachments; /** The key for the usage object's input tokens * @type {string} */ this.inputTokensKey = 'prompt_tokens'; @@ -629,6 +631,10 @@ class BaseClient { await this.userMessagePromise; } + if (this.artifactPromises) { + responseMessage.attachments = (await Promise.all(this.artifactPromises)).filter((a) => a); + } + this.responsePromise = this.saveMessageToDatabase(responseMessage, saveOptions, user); const messageCache = getLogStores(CacheKeys.MESSAGES); messageCache.set( diff --git a/api/app/clients/prompts/formatAgentMessages.spec.js b/api/app/clients/prompts/formatAgentMessages.spec.js new file mode 100644 index 0000000000..fe0f1e0228 --- /dev/null +++ b/api/app/clients/prompts/formatAgentMessages.spec.js @@ -0,0 +1,285 @@ +const { ToolMessage } = require('@langchain/core/messages'); +const { ContentTypes } = require('librechat-data-provider'); +const { HumanMessage, AIMessage, SystemMessage } = require('langchain/schema'); +const { formatAgentMessages } = require('./formatMessages'); + +describe('formatAgentMessages', () => { + it('should format simple user and AI messages', () => { + const payload = [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi there!' }, + ]; + const result = formatAgentMessages(payload); + expect(result).toHaveLength(2); + expect(result[0]).toBeInstanceOf(HumanMessage); + expect(result[1]).toBeInstanceOf(AIMessage); + }); + + it('should handle system messages', () => { + const payload = [{ role: 'system', content: 'You are a helpful assistant.' }]; + const result = formatAgentMessages(payload); + expect(result).toHaveLength(1); + expect(result[0]).toBeInstanceOf(SystemMessage); + }); + + it('should format messages with content arrays', () => { + const payload = [ + { + role: 'user', + content: [{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hello' }], + }, + ]; + const result = formatAgentMessages(payload); + expect(result).toHaveLength(1); + expect(result[0]).toBeInstanceOf(HumanMessage); + }); + + it('should handle tool calls and create ToolMessages', () => { + const payload = [ + { + role: 'assistant', + content: [ + { + type: ContentTypes.TEXT, + [ContentTypes.TEXT]: 'Let me check that for you.', + tool_call_ids: ['123'], + }, + { + type: ContentTypes.TOOL_CALL, + tool_call: { + id: '123', + name: 'search', + args: '{"query":"weather"}', + output: 'The weather is sunny.', + }, + }, + ], + }, + ]; + const result = formatAgentMessages(payload); + expect(result).toHaveLength(2); + expect(result[0]).toBeInstanceOf(AIMessage); + expect(result[1]).toBeInstanceOf(ToolMessage); + expect(result[0].tool_calls).toHaveLength(1); + expect(result[1].tool_call_id).toBe('123'); + }); + + it('should handle multiple content parts in assistant messages', () => { + const payload = [ + { + role: 'assistant', + content: [ + { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Part 1' }, + { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Part 2' }, + ], + }, + ]; + const result = formatAgentMessages(payload); + expect(result).toHaveLength(1); + expect(result[0]).toBeInstanceOf(AIMessage); + expect(result[0].content).toHaveLength(2); + }); + + it('should throw an error for invalid tool call structure', () => { + const payload = [ + { + role: 'assistant', + content: [ + { + type: ContentTypes.TOOL_CALL, + tool_call: { + id: '123', + name: 'search', + args: '{"query":"weather"}', + output: 'The weather is sunny.', + }, + }, + ], + }, + ]; + expect(() => formatAgentMessages(payload)).toThrow('Invalid tool call structure'); + }); + + it('should handle tool calls with non-JSON args', () => { + const payload = [ + { + role: 'assistant', + content: [ + { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Checking...', tool_call_ids: ['123'] }, + { + type: ContentTypes.TOOL_CALL, + tool_call: { + id: '123', + name: 'search', + args: 'non-json-string', + output: 'Result', + }, + }, + ], + }, + ]; + const result = formatAgentMessages(payload); + expect(result).toHaveLength(2); + expect(result[0].tool_calls[0].args).toBe('non-json-string'); + }); + + it('should handle complex tool calls with multiple steps', () => { + const payload = [ + { + role: 'assistant', + content: [ + { + type: ContentTypes.TEXT, + [ContentTypes.TEXT]: 'I\'ll search for that information.', + tool_call_ids: ['search_1'], + }, + { + type: ContentTypes.TOOL_CALL, + tool_call: { + id: 'search_1', + name: 'search', + args: '{"query":"weather in New York"}', + output: 'The weather in New York is currently sunny with a temperature of 75°F.', + }, + }, + { + type: ContentTypes.TEXT, + [ContentTypes.TEXT]: 'Now, I\'ll convert the temperature.', + tool_call_ids: ['convert_1'], + }, + { + type: ContentTypes.TOOL_CALL, + tool_call: { + id: 'convert_1', + name: 'convert_temperature', + args: '{"temperature": 75, "from": "F", "to": "C"}', + output: '23.89°C', + }, + }, + { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Here\'s your answer.' }, + ], + }, + ]; + + const result = formatAgentMessages(payload); + + expect(result).toHaveLength(5); + expect(result[0]).toBeInstanceOf(AIMessage); + expect(result[1]).toBeInstanceOf(ToolMessage); + expect(result[2]).toBeInstanceOf(AIMessage); + expect(result[3]).toBeInstanceOf(ToolMessage); + expect(result[4]).toBeInstanceOf(AIMessage); + + // Check first AIMessage + expect(result[0].content).toBe('I\'ll search for that information.'); + expect(result[0].tool_calls).toHaveLength(1); + expect(result[0].tool_calls[0]).toEqual({ + id: 'search_1', + name: 'search', + args: { query: 'weather in New York' }, + }); + + // Check first ToolMessage + expect(result[1].tool_call_id).toBe('search_1'); + expect(result[1].name).toBe('search'); + expect(result[1].content).toBe( + 'The weather in New York is currently sunny with a temperature of 75°F.', + ); + + // Check second AIMessage + expect(result[2].content).toBe('Now, I\'ll convert the temperature.'); + expect(result[2].tool_calls).toHaveLength(1); + expect(result[2].tool_calls[0]).toEqual({ + id: 'convert_1', + name: 'convert_temperature', + args: { temperature: 75, from: 'F', to: 'C' }, + }); + + // Check second ToolMessage + expect(result[3].tool_call_id).toBe('convert_1'); + expect(result[3].name).toBe('convert_temperature'); + expect(result[3].content).toBe('23.89°C'); + + // Check final AIMessage + expect(result[4].content).toStrictEqual([ + { [ContentTypes.TEXT]: 'Here\'s your answer.', type: ContentTypes.TEXT }, + ]); + }); + + it.skip('should not produce two consecutive assistant messages and format content correctly', () => { + const payload = [ + { role: 'user', content: 'Hello' }, + { + role: 'assistant', + content: [{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hi there!' }], + }, + { + role: 'assistant', + content: [{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'How can I help you?' }], + }, + { role: 'user', content: 'What\'s the weather?' }, + { + role: 'assistant', + content: [ + { + type: ContentTypes.TEXT, + [ContentTypes.TEXT]: 'Let me check that for you.', + tool_call_ids: ['weather_1'], + }, + { + type: ContentTypes.TOOL_CALL, + tool_call: { + id: 'weather_1', + name: 'check_weather', + args: '{"location":"New York"}', + output: 'Sunny, 75°F', + }, + }, + ], + }, + { + role: 'assistant', + content: [ + { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Here\'s the weather information.' }, + ], + }, + ]; + + const result = formatAgentMessages(payload); + + // Check correct message count and types + expect(result).toHaveLength(6); + expect(result[0]).toBeInstanceOf(HumanMessage); + expect(result[1]).toBeInstanceOf(AIMessage); + expect(result[2]).toBeInstanceOf(HumanMessage); + expect(result[3]).toBeInstanceOf(AIMessage); + expect(result[4]).toBeInstanceOf(ToolMessage); + expect(result[5]).toBeInstanceOf(AIMessage); + + // Check content of messages + expect(result[0].content).toStrictEqual([ + { [ContentTypes.TEXT]: 'Hello', type: ContentTypes.TEXT }, + ]); + expect(result[1].content).toStrictEqual([ + { [ContentTypes.TEXT]: 'Hi there!', type: ContentTypes.TEXT }, + { [ContentTypes.TEXT]: 'How can I help you?', type: ContentTypes.TEXT }, + ]); + expect(result[2].content).toStrictEqual([ + { [ContentTypes.TEXT]: 'What\'s the weather?', type: ContentTypes.TEXT }, + ]); + expect(result[3].content).toBe('Let me check that for you.'); + expect(result[4].content).toBe('Sunny, 75°F'); + expect(result[5].content).toStrictEqual([ + { [ContentTypes.TEXT]: 'Here\'s the weather information.', type: ContentTypes.TEXT }, + ]); + + // Check that there are no consecutive AIMessages + const messageTypes = result.map((message) => message.constructor); + for (let i = 0; i < messageTypes.length - 1; i++) { + expect(messageTypes[i] === AIMessage && messageTypes[i + 1] === AIMessage).toBe(false); + } + + // Additional check to ensure the consecutive assistant messages were combined + expect(result[1].content).toHaveLength(2); + }); +}); diff --git a/api/app/clients/prompts/formatMessages.js b/api/app/clients/prompts/formatMessages.js index 1ea44f9d29..29784d6531 100644 --- a/api/app/clients/prompts/formatMessages.js +++ b/api/app/clients/prompts/formatMessages.js @@ -155,10 +155,22 @@ const formatAgentMessages = (payload) => { for (const part of message.content) { if (part.type === ContentTypes.TEXT && part.tool_call_ids) { - // If there's pending content, add it as an AIMessage + /* + If there's pending content, it needs to be aggregated as a single string to prepare for tool calls. + For Anthropic models, the "tool_calls" field on a message is only respected if content is a string. + */ if (currentContent.length > 0) { - messages.push(new AIMessage({ content: currentContent })); + let content = currentContent.reduce((acc, curr) => { + if (curr.type === ContentTypes.TEXT) { + return `${acc}${curr[ContentTypes.TEXT]}\n`; + } + return acc; + }, ''); + content = `${content}\n${part[ContentTypes.TEXT] ?? ''}`.trim(); + lastAIMessage = new AIMessage({ content }); + messages.push(lastAIMessage); currentContent = []; + continue; } // Create a new AIMessage with this text and prepare for tool calls diff --git a/api/app/clients/tools/index.js b/api/app/clients/tools/index.js index f16d229e6b..6cc31237df 100644 --- a/api/app/clients/tools/index.js +++ b/api/app/clients/tools/index.js @@ -25,7 +25,6 @@ module.exports = { // Basic Tools CodeBrew, AzureAiSearch, - GoogleSearchAPI, WolframAlphaAPI, OpenAICreateImage, StableDiffusionAPI, @@ -37,8 +36,9 @@ module.exports = { CodeSherpa, StructuredSD, StructuredACS, + GoogleSearchAPI, CodeSherpaTools, + TraversaalSearch, StructuredWolfram, TavilySearchResults, - TraversaalSearch, }; diff --git a/api/app/clients/tools/structured/AzureAISearch.js b/api/app/clients/tools/structured/AzureAISearch.js index 1a8c3e9e6e..e25da94426 100644 --- a/api/app/clients/tools/structured/AzureAISearch.js +++ b/api/app/clients/tools/structured/AzureAISearch.js @@ -1,9 +1,9 @@ const { z } = require('zod'); -const { StructuredTool } = require('langchain/tools'); +const { Tool } = require('@langchain/core/tools'); const { SearchClient, AzureKeyCredential } = require('@azure/search-documents'); const { logger } = require('~/config'); -class AzureAISearch extends StructuredTool { +class AzureAISearch extends Tool { // Constants for default values static DEFAULT_API_VERSION = '2023-11-01'; static DEFAULT_QUERY_TYPE = 'simple'; diff --git a/api/app/clients/tools/structured/DALLE3.js b/api/app/clients/tools/structured/DALLE3.js index 3155992ca9..8cfeaf8416 100644 --- a/api/app/clients/tools/structured/DALLE3.js +++ b/api/app/clients/tools/structured/DALLE3.js @@ -2,7 +2,7 @@ const { z } = require('zod'); const path = require('path'); const OpenAI = require('openai'); const { v4: uuidv4 } = require('uuid'); -const { Tool } = require('langchain/tools'); +const { Tool } = require('@langchain/core/tools'); const { HttpsProxyAgent } = require('https-proxy-agent'); const { FileContext } = require('librechat-data-provider'); const { getImageBasename } = require('~/server/services/Files/images'); diff --git a/api/app/clients/tools/structured/GoogleSearch.js b/api/app/clients/tools/structured/GoogleSearch.js index c2cf72b9d6..d703d56f83 100644 --- a/api/app/clients/tools/structured/GoogleSearch.js +++ b/api/app/clients/tools/structured/GoogleSearch.js @@ -4,11 +4,12 @@ const { getEnvironmentVariable } = require('@langchain/core/utils/env'); class GoogleSearchResults extends Tool { static lc_name() { - return 'GoogleSearchResults'; + return 'google'; } constructor(fields = {}) { super(fields); + this.name = 'google'; this.envVarApiKey = 'GOOGLE_SEARCH_API_KEY'; this.envVarSearchEngineId = 'GOOGLE_CSE_ID'; this.override = fields.override ?? false; diff --git a/api/app/clients/tools/structured/StableDiffusion.js b/api/app/clients/tools/structured/StableDiffusion.js index cfcbf73ac4..6309da35d8 100644 --- a/api/app/clients/tools/structured/StableDiffusion.js +++ b/api/app/clients/tools/structured/StableDiffusion.js @@ -5,12 +5,12 @@ const path = require('path'); const axios = require('axios'); const sharp = require('sharp'); const { v4: uuidv4 } = require('uuid'); -const { StructuredTool } = require('langchain/tools'); +const { Tool } = require('@langchain/core/tools'); const { FileContext } = require('librechat-data-provider'); const paths = require('~/config/paths'); const { logger } = require('~/config'); -class StableDiffusionAPI extends StructuredTool { +class StableDiffusionAPI extends Tool { constructor(fields) { super(); /** @type {string} User ID */ diff --git a/api/app/clients/tools/structured/Wolfram.js b/api/app/clients/tools/structured/Wolfram.js index fc857b35cb..1b426298cc 100644 --- a/api/app/clients/tools/structured/Wolfram.js +++ b/api/app/clients/tools/structured/Wolfram.js @@ -1,10 +1,10 @@ /* eslint-disable no-useless-escape */ const axios = require('axios'); const { z } = require('zod'); -const { StructuredTool } = require('langchain/tools'); +const { Tool } = require('@langchain/core/tools'); const { logger } = require('~/config'); -class WolframAlphaAPI extends StructuredTool { +class WolframAlphaAPI extends Tool { constructor(fields) { super(); /* Used to initialize the Tool without necessary variables. */ diff --git a/api/app/clients/tools/util/createFileSearchTool.js b/api/app/clients/tools/util/createFileSearchTool.js new file mode 100644 index 0000000000..f00e4757f6 --- /dev/null +++ b/api/app/clients/tools/util/createFileSearchTool.js @@ -0,0 +1,104 @@ +const { z } = require('zod'); +const axios = require('axios'); +const { tool } = require('@langchain/core/tools'); +const { Tools, EToolResources } = require('librechat-data-provider'); +const { getFiles } = require('~/models/File'); +const { logger } = require('~/config'); + +/** + * + * @param {Object} options + * @param {ServerRequest} options.req + * @param {Agent['tool_resources']} options.tool_resources + * @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( + async ({ query }) => { + if (files.length === 0) { + return 'No files to search. Instruct the user to add files for the search.'; + } + const jwtToken = req.headers.authorization.split(' ')[1]; + if (!jwtToken) { + return 'There was an error authenticating the file search request.'; + } + const queryPromises = files.map((file) => + axios + .post( + `${process.env.RAG_API_URL}/query`, + { + file_id: file.file_id, + query, + k: 5, + }, + { + headers: { + Authorization: `Bearer ${jwtToken}`, + 'Content-Type': 'application/json', + }, + }, + ) + .catch((error) => { + logger.error( + `Error encountered in \`file_search\` while querying file_id ${file._id}:`, + error, + ); + return null; + }), + ); + + const results = await Promise.all(queryPromises); + const validResults = results.filter((result) => result !== null); + + if (validResults.length === 0) { + return 'No results found or errors occurred while searching the files.'; + } + + const formattedResults = validResults + .flatMap((result) => + result.data.map(([docInfo, relevanceScore]) => ({ + filename: docInfo.metadata.source.split('/').pop(), + content: docInfo.page_content, + relevanceScore, + })), + ) + .sort((a, b) => b.relevanceScore - a.relevanceScore); + + const formattedString = formattedResults + .map( + (result) => + `File: ${result.filename}\nRelevance: ${result.relevanceScore.toFixed(4)}\nContent: ${ + result.content + }\n`, + ) + .join('\n---\n'); + + return formattedString; + }, + { + name: Tools.file_search, + description: toolDescription, + schema: z.object({ + query: z + .string() + .describe( + 'A natural language query to search for relevant information in the files. Be specific and use keywords related to the information you\'re looking for. The query will be used for semantic similarity matching against the file contents.', + ), + }), + }, + ); + + return FileSearch; +}; + +module.exports = createFileSearchTool; diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index 7ed1865871..8aeb401950 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -1,8 +1,10 @@ +const { Tools } = require('librechat-data-provider'); const { ZapierToolKit } = require('langchain/agents'); const { Calculator } = require('langchain/tools/calculator'); const { WebBrowser } = require('langchain/tools/webbrowser'); const { SerpAPI, ZapierNLAWrapper } = require('langchain/tools'); const { OpenAIEmbeddings } = require('langchain/embeddings/openai'); +const { createCodeExecutionTool, EnvVar } = require('@librechat/agents'); const { getUserPluginAuthValue } = require('~/server/services/PluginService'); const { availableTools, @@ -24,6 +26,7 @@ const { StructuredWolfram, TavilySearchResults, } = require('../'); +const createFileSearchTool = require('./createFileSearchTool'); const { loadToolSuite } = require('./loadToolSuite'); const { loadSpecs } = require('./loadSpecs'); const { logger } = require('~/config'); @@ -97,6 +100,45 @@ const validateTools = async (user, tools = []) => { } }; +const loadAuthValues = async ({ userId, authFields }) => { + let authValues = {}; + + /** + * Finds the first non-empty value for the given authentication field, supporting alternate fields. + * @param {string[]} fields Array of strings representing the authentication fields. Supports alternate fields delimited by "||". + * @returns {Promise<{ authField: string, authValue: string} | null>} An object containing the authentication field and value, or null if not found. + */ + const findAuthValue = async (fields) => { + for (const field of fields) { + let value = process.env[field]; + if (value) { + return { authField: field, authValue: value }; + } + try { + value = await getUserPluginAuthValue(userId, field); + } catch (err) { + if (field === fields[fields.length - 1] && !value) { + throw err; + } + } + if (value) { + return { authField: field, authValue: value }; + } + } + return null; + }; + + for (let authField of authFields) { + const fields = authField.split('||'); + const result = await findAuthValue(fields); + if (result) { + authValues[result.authField] = result.authValue; + } + } + + return authValues; +}; + /** * 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. @@ -109,41 +151,7 @@ const validateTools = async (user, tools = []) => { */ const loadToolWithAuth = (userId, authFields, ToolConstructor, options = {}) => { return async function () { - let authValues = {}; - - /** - * Finds the first non-empty value for the given authentication field, supporting alternate fields. - * @param {string[]} fields Array of strings representing the authentication fields. Supports alternate fields delimited by "||". - * @returns {Promise<{ authField: string, authValue: string} | null>} An object containing the authentication field and value, or null if not found. - */ - const findAuthValue = async (fields) => { - for (const field of fields) { - let value = process.env[field]; - if (value) { - return { authField: field, authValue: value }; - } - try { - value = await getUserPluginAuthValue(userId, field); - } catch (err) { - if (field === fields[fields.length - 1] && !value) { - throw err; - } - } - if (value) { - return { authField: field, authValue: value }; - } - } - return null; - }; - - for (let authField of authFields) { - const fields = authField.split('||'); - const result = await findAuthValue(fields); - if (result) { - authValues[result.authField] = result.authValue; - } - } - + const authValues = await loadAuthValues({ userId, authFields }); return new ToolConstructor({ ...options, ...authValues, userId }); }; }; @@ -264,6 +272,22 @@ const loadTools = async ({ const remainingTools = []; for (const tool of tools) { + if (tool === Tools.execute_code) { + const authValues = await loadAuthValues({ + userId: user.id, + authFields: [EnvVar.CODE_API_KEY], + }); + requestedTools[tool] = () => + createCodeExecutionTool({ + user_id: user.id, + ...authValues, + }); + continue; + } else if (tool === Tools.file_search) { + requestedTools[tool] = () => createFileSearchTool(options); + continue; + } + if (customConstructors[tool]) { requestedTools[tool] = customConstructors[tool]; continue; @@ -331,6 +355,7 @@ const loadTools = async ({ module.exports = { loadToolWithAuth, + loadAuthValues, validateTools, loadTools, }; diff --git a/api/app/clients/tools/util/index.js b/api/app/clients/tools/util/index.js index ea67bb4ced..73d10270b6 100644 --- a/api/app/clients/tools/util/index.js +++ b/api/app/clients/tools/util/index.js @@ -1,8 +1,9 @@ -const { validateTools, loadTools } = require('./handleTools'); +const { validateTools, loadTools, loadAuthValues } = require('./handleTools'); const handleOpenAIErrors = require('./handleOpenAIErrors'); module.exports = { handleOpenAIErrors, + loadAuthValues, validateTools, loadTools, }; diff --git a/api/models/Agent.js b/api/models/Agent.js index 2112a44991..7d599d3032 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -1,11 +1,14 @@ const mongoose = require('mongoose'); +const { SystemRoles } = require('librechat-data-provider'); const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants; +const { CONFIG_STORE, STARTUP_CONFIG } = require('librechat-data-provider').CacheKeys; const { getProjectByName, addAgentIdsToProject, removeAgentIdsFromProject, removeAgentFromAllProjects, } = require('./Project'); +const getLogStores = require('~/cache/getLogStores'); const agentSchema = require('./schema/agent'); const Agent = mongoose.model('agent', agentSchema); @@ -30,6 +33,43 @@ const createAgent = async (agentData) => { */ const getAgent = async (searchParameter) => await Agent.findOne(searchParameter).lean(); +/** + * Load an agent based on the provided ID + * + * @param {Object} params + * @param {ServerRequest} params.req + * @param {string} params.agent_id + * @returns {Promise} The agent document as a plain object, or null if not found. + */ +const loadAgent = async ({ req, agent_id }) => { + const agent = await getAgent({ + id: agent_id, + }); + + if (agent.author.toString() === req.user.id) { + return agent; + } + + if (!agent.projectIds) { + return null; + } + + const cache = getLogStores(CONFIG_STORE); + /** @type {TStartupConfig} */ + const cachedStartupConfig = await cache.get(STARTUP_CONFIG); + let { instanceProjectId } = cachedStartupConfig ?? {}; + if (!instanceProjectId) { + instanceProjectId = (await getProjectByName(GLOBAL_PROJECT_NAME, '_id'))._id.toString(); + } + + for (const projectObjectId of agent.projectIds) { + const projectId = projectObjectId.toString(); + if (projectId === instanceProjectId) { + return agent; + } + } +}; + /** * Update an agent with new data without overwriting existing * properties, or create a new agent if it doesn't exist. @@ -41,10 +81,76 @@ const getAgent = async (searchParameter) => await Agent.findOne(searchParameter) * @returns {Promise} The updated or newly created agent document as a plain object. */ const updateAgent = async (searchParameter, updateData) => { - const options = { new: true, upsert: true }; + const options = { new: true, upsert: false }; return await Agent.findOneAndUpdate(searchParameter, updateData, options).lean(); }; +/** + * Modifies an agent with the resource file id. + * @param {object} params + * @param {ServerRequest} params.req + * @param {string} params.agent_id + * @param {string} params.tool_resource + * @param {string} params.file_id + * @returns {Promise} The updated agent. + */ +const addAgentResourceFile = async ({ agent_id, tool_resource, file_id }) => { + const searchParameter = { id: agent_id }; + const agent = await getAgent(searchParameter); + + if (!agent) { + throw new Error('Agent not found for adding resource file'); + } + + const tool_resources = agent.tool_resources || {}; + + if (!tool_resources[tool_resource]) { + tool_resources[tool_resource] = { file_ids: [] }; + } + + if (!tool_resources[tool_resource].file_ids.includes(file_id)) { + tool_resources[tool_resource].file_ids.push(file_id); + } + + const updateData = { tool_resources }; + + return await updateAgent(searchParameter, updateData); +}; + +/** + * Removes a resource file id from an agent. + * @param {object} params + * @param {ServerRequest} params.req + * @param {string} params.agent_id + * @param {string} params.tool_resource + * @param {string} params.file_id + * @returns {Promise} The updated agent. + */ +const removeAgentResourceFile = async ({ agent_id, tool_resource, file_id }) => { + const searchParameter = { id: agent_id }; + const agent = await getAgent(searchParameter); + + if (!agent) { + throw new Error('Agent not found for removing resource file'); + } + + 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 updateData = { tool_resources }; + + return await updateAgent(searchParameter, updateData); +}; + /** * Deletes an agent based on the provided ID. * @@ -79,12 +185,25 @@ const getListAgents = async (searchParameter) => { query = { $or: [globalQuery, query] }; } - const agents = await Agent.find(query, { - id: 1, - name: 1, - avatar: 1, - projectIds: 1, - }).lean(); + const agents = ( + await Agent.find(query, { + id: 1, + _id: 0, + name: 1, + avatar: 1, + author: 1, + projectIds: 1, + isCollaborative: 1, + }).lean() + ).map((agent) => { + if (agent.author?.toString() !== author) { + delete agent.author; + } + if (agent.author) { + agent.author = agent.author.toString(); + } + return agent; + }); const hasMore = agents.length > 0; const firstId = agents.length > 0 ? agents[0].id : null; @@ -102,13 +221,15 @@ const getListAgents = async (searchParameter) => { * Updates the projects associated with an agent, adding and removing project IDs as specified. * This function also updates the corresponding projects to include or exclude the agent ID. * - * @param {string} agentId - The ID of the agent to update. - * @param {string[]} [projectIds] - Array of project IDs to add to the agent. - * @param {string[]} [removeProjectIds] - Array of project IDs to remove from the agent. + * @param {Object} params - Parameters for updating the agent's projects. + * @param {import('librechat-data-provider').TUser} params.user - Parameters for updating the agent's projects. + * @param {string} params.agentId - The ID of the agent to update. + * @param {string[]} [params.projectIds] - Array of project IDs to add to the agent. + * @param {string[]} [params.removeProjectIds] - Array of project IDs to remove from the agent. * @returns {Promise} The updated agent document. * @throws {Error} If there's an error updating the agent or projects. */ -const updateAgentProjects = async (agentId, projectIds, removeProjectIds) => { +const updateAgentProjects = async ({ user, agentId, projectIds, removeProjectIds }) => { const updateOps = {}; if (removeProjectIds && removeProjectIds.length > 0) { @@ -129,14 +250,36 @@ const updateAgentProjects = async (agentId, projectIds, removeProjectIds) => { return await getAgent({ id: agentId }); } - return await updateAgent({ id: agentId }, updateOps); + const updateQuery = { id: agentId, author: user.id }; + if (user.role === SystemRoles.ADMIN) { + delete updateQuery.author; + } + + const updatedAgent = await updateAgent(updateQuery, updateOps); + if (updatedAgent) { + return updatedAgent; + } + if (updateOps.$addToSet) { + for (const projectId of projectIds) { + await removeAgentIdsFromProject(projectId, [agentId]); + } + } else if (updateOps.$pull) { + for (const projectId of removeProjectIds) { + await addAgentIdsToProject(projectId, [agentId]); + } + } + + return await getAgent({ id: agentId }); }; module.exports = { - createAgent, getAgent, + loadAgent, + createAgent, updateAgent, deleteAgent, getListAgents, updateAgentProjects, + addAgentResourceFile, + removeAgentResourceFile, }; diff --git a/api/models/schema/agent.js b/api/models/schema/agent.js index 819398ee7c..d7c5762b53 100644 --- a/api/models/schema/agent.js +++ b/api/models/schema/agent.js @@ -5,6 +5,7 @@ const agentSchema = mongoose.Schema( id: { type: String, index: true, + unique: true, required: true, }, name: { @@ -44,10 +45,6 @@ const agentSchema = mongoose.Schema( tool_kwargs: { type: [{ type: mongoose.Schema.Types.Mixed }], }, - file_ids: { - type: [String], - default: undefined, - }, actions: { type: [String], default: undefined, @@ -57,6 +54,22 @@ const agentSchema = mongoose.Schema( ref: 'User', required: true, }, + authorName: { + type: String, + default: undefined, + }, + isCollaborative: { + type: Boolean, + default: undefined, + }, + conversation_starters: { + type: [String], + default: [], + }, + tool_resources: { + type: mongoose.Schema.Types.Mixed, + default: {}, + }, projectIds: { type: [mongoose.Schema.Types.ObjectId], ref: 'Project', diff --git a/api/models/schema/messageSchema.js b/api/models/schema/messageSchema.js index dddbdb6ebe..5d2e43a3ee 100644 --- a/api/models/schema/messageSchema.js +++ b/api/models/schema/messageSchema.js @@ -115,6 +115,29 @@ const messageSchema = mongoose.Schema( iconURL: { type: String, }, + attachments: { type: [{ type: mongoose.Schema.Types.Mixed }], default: undefined }, + /* + attachments: { + type: [ + { + file_id: String, + filename: String, + filepath: String, + expiresAt: Date, + width: Number, + height: Number, + type: String, + conversationId: String, + messageId: { + type: String, + required: true, + }, + toolCallId: String, + }, + ], + default: undefined, + }, + */ }, { timestamps: true }, ); diff --git a/api/package.json b/api/package.json index 1c74543a67..7b9ac4d325 100644 --- a/api/package.json +++ b/api/package.json @@ -43,7 +43,7 @@ "@langchain/core": "^0.2.18", "@langchain/google-genai": "^0.0.11", "@langchain/google-vertexai": "^0.0.17", - "@librechat/agents": "^1.5.2", + "@librechat/agents": "^1.6.9", "axios": "^1.7.7", "bcryptjs": "^2.4.3", "cheerio": "^1.0.0-rc.12", diff --git a/api/server/controllers/agents/callbacks.js b/api/server/controllers/agents/callbacks.js index f6c1972b4f..7bbb2212aa 100644 --- a/api/server/controllers/agents/callbacks.js +++ b/api/server/controllers/agents/callbacks.js @@ -1,8 +1,13 @@ +const { Tools } = require('librechat-data-provider'); const { GraphEvents, ToolEndHandler, ChatModelStreamHandler } = require('@librechat/agents'); +const { processCodeOutput } = require('~/server/services/Files/Code/process'); +const { logger } = require('~/config'); /** @typedef {import('@librechat/agents').Graph} Graph */ /** @typedef {import('@librechat/agents').EventHandler} EventHandler */ /** @typedef {import('@librechat/agents').ModelEndData} ModelEndData */ +/** @typedef {import('@librechat/agents').ToolEndData} ToolEndData */ +/** @typedef {import('@librechat/agents').ToolEndCallback} ToolEndCallback */ /** @typedef {import('@librechat/agents').ChatModelStreamHandler} ChatModelStreamHandler */ /** @typedef {import('@librechat/agents').ContentAggregatorResult['aggregateContent']} ContentAggregator */ /** @typedef {import('@librechat/agents').GraphEvents} GraphEvents */ @@ -58,11 +63,12 @@ class ModelEndHandler { * @param {Object} options - The options object. * @param {ServerResponse} options.res - The options object. * @param {ContentAggregator} options.aggregateContent - The options object. + * @param {ToolEndCallback} options.toolEndCallback - Callback to use when tool ends. * @param {Array} options.collectedUsage - The list of collected usage metadata. * @returns {Record} The default handlers. * @throws {Error} If the request is not found. */ -function getDefaultHandlers({ res, aggregateContent, collectedUsage }) { +function getDefaultHandlers({ res, aggregateContent, toolEndCallback, collectedUsage }) { if (!res || !aggregateContent) { throw new Error( `[getDefaultHandlers] Missing required options: res: ${!res}, aggregateContent: ${!aggregateContent}`, @@ -70,7 +76,7 @@ function getDefaultHandlers({ res, aggregateContent, collectedUsage }) { } const handlers = { [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage), - [GraphEvents.TOOL_END]: new ToolEndHandler(), + [GraphEvents.TOOL_END]: new ToolEndHandler(toolEndCallback), [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(), [GraphEvents.ON_RUN_STEP]: { /** @@ -121,7 +127,67 @@ function getDefaultHandlers({ res, aggregateContent, collectedUsage }) { return handlers; } +/** + * + * @param {Object} params + * @param {ServerRequest} params.req + * @param {ServerResponse} params.res + * @param {Promise[]} params.artifactPromises + * @returns {ToolEndCallback} The tool end callback. + */ +function createToolEndCallback({ req, res, artifactPromises }) { + /** + * @type {ToolEndCallback} + */ + return async (data, metadata) => { + const output = data?.output; + if (!output) { + return; + } + + if (output.name !== Tools.execute_code) { + return; + } + + const { tool_call_id, artifact } = output; + if (!artifact.files) { + return; + } + + for (const file of artifact.files) { + const { id, name } = file; + artifactPromises.push( + (async () => { + const fileMetadata = await processCodeOutput({ + req, + id, + name, + toolCallId: tool_call_id, + messageId: metadata.run_id, + sessionId: artifact.session_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; + }), + ); + } + }; +} + module.exports = { sendEvent, getDefaultHandlers, + createToolEndCallback, }; diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 137068ddd6..101424d5d7 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -10,7 +10,9 @@ const { Callback, createMetadataAggregator } = require('@librechat/agents'); const { Constants, + openAISchema, EModelEndpoint, + anthropicSchema, bedrockOutputParser, providerEndpointMap, removeNullishValues, @@ -35,11 +37,10 @@ const { logger } = require('~/config'); /** @typedef {import('@librechat/agents').MessageContentComplex} MessageContentComplex */ -// const providerSchemas = { -// [EModelEndpoint.bedrock]: true, -// }; - const providerParsers = { + [EModelEndpoint.openAI]: openAISchema, + [EModelEndpoint.azureOpenAI]: openAISchema, + [EModelEndpoint.anthropic]: anthropicSchema, [EModelEndpoint.bedrock]: bedrockOutputParser, }; @@ -57,10 +58,11 @@ class AgentClient extends BaseClient { this.run; const { - maxContextTokens, - modelOptions = {}, contentParts, collectedUsage, + artifactPromises, + maxContextTokens, + modelOptions = {}, ...clientOptions } = options; @@ -70,6 +72,8 @@ class AgentClient extends BaseClient { this.contentParts = contentParts; /** @type {Array} */ this.collectedUsage = collectedUsage; + /** @type {ArtifactPromises} */ + this.artifactPromises = artifactPromises; this.options = Object.assign({ endpoint: options.endpoint }, clientOptions); } @@ -477,7 +481,6 @@ class AgentClient extends BaseClient { provider: providerEndpointMap[this.options.agent.provider], thread_id: this.conversationId, }, - run_id: this.responseMessageId, signal: abortController.signal, streamMode: 'values', version: 'v2', diff --git a/api/server/controllers/agents/run.js b/api/server/controllers/agents/run.js index 5aeefa122d..2377ce64f1 100644 --- a/api/server/controllers/agents/run.js +++ b/api/server/controllers/agents/run.js @@ -45,10 +45,9 @@ async function createRun({ ); const graphConfig = { - runId, - llmConfig, tools, toolMap, + llmConfig, instructions: agent.instructions, additional_instructions: agent.additional_instructions, }; @@ -59,6 +58,7 @@ async function createRun({ } return Run.create({ + runId, graphConfig, customHandlers, }); diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 65e37f2618..2aa416f840 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -1,5 +1,5 @@ const { nanoid } = require('nanoid'); -const { FileContext, Constants } = require('librechat-data-provider'); +const { FileContext, Constants, Tools, SystemRoles } = require('librechat-data-provider'); const { getAgent, createAgent, @@ -14,6 +14,11 @@ const { updateAgentProjects } = require('~/models/Agent'); const { deleteFileByFilter } = require('~/models/File'); const { logger } = require('~/config'); +const systemTools = { + [Tools.execute_code]: true, + [Tools.file_search]: true, +}; + /** * Creates an Agent. * @route POST /Agents @@ -27,9 +32,17 @@ const createAgentHandler = async (req, res) => { const { tools = [], provider, name, description, instructions, model, ...agentData } = req.body; const { id: userId } = req.user; - agentData.tools = tools - .map((tool) => (typeof tool === 'string' ? req.app.locals.availableTools[tool] : tool)) - .filter(Boolean); + agentData.tools = []; + + for (const tool of tools) { + if (req.app.locals.availableTools[tool]) { + agentData.tools.push(tool); + } + + if (systemTools[tool]) { + agentData.tools.push(tool); + } + } Object.assign(agentData, { author: userId, @@ -80,10 +93,24 @@ const getAgentHandler = async (req, res) => { return res.status(404).json({ error: 'Agent not found' }); } + agent.author = agent.author.toString(); + agent.isCollaborative = !!agent.isCollaborative; + if (agent.author !== author) { delete agent.author; } + if (!agent.isCollaborative && agent.author !== author && req.user.role !== SystemRoles.ADMIN) { + return res.status(200).json({ + id: agent.id, + name: agent.name, + avatar: agent.avatar, + author: agent.author, + projectIds: agent.projectIds, + isCollaborative: agent.isCollaborative, + }); + } + return res.status(200).json(agent); } catch (error) { logger.error('[/Agents/:id] Error retrieving agent', error); @@ -106,12 +133,29 @@ const updateAgentHandler = async (req, res) => { const { projectIds, removeProjectIds, ...updateData } = req.body; let updatedAgent; + const query = { id, author: req.user.id }; + if (req.user.role === SystemRoles.ADMIN) { + delete query.author; + } if (Object.keys(updateData).length > 0) { - updatedAgent = await updateAgent({ id, author: req.user.id }, updateData); + updatedAgent = await updateAgent(query, updateData); } if (projectIds || removeProjectIds) { - updatedAgent = await updateAgentProjects(id, projectIds, removeProjectIds); + updatedAgent = await updateAgentProjects({ + user: req.user, + agentId: id, + projectIds, + removeProjectIds, + }); + } + + if (updatedAgent.author) { + updatedAgent.author = updatedAgent.author.toString(); + } + + if (updatedAgent.author !== req.user.id) { + delete updatedAgent.author; } return res.json(updatedAgent); diff --git a/api/server/controllers/assistants/v2.js b/api/server/controllers/assistants/v2.js index 0e1b4a6412..7d91e55b98 100644 --- a/api/server/controllers/assistants/v2.js +++ b/api/server/controllers/assistants/v2.js @@ -149,7 +149,6 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => { * @param {string} params.assistant_id * @param {string} params.tool_resource * @param {string} params.file_id - * @param {AssistantUpdateParams} params.updateData * @returns {Promise} The updated assistant. */ const addResourceFileId = async ({ req, openai, assistant_id, tool_resource, file_id }) => { diff --git a/api/server/routes/agents/chat.js b/api/server/routes/agents/chat.js index 353c2fc7b1..8302abcde0 100644 --- a/api/server/routes/agents/chat.js +++ b/api/server/routes/agents/chat.js @@ -10,6 +10,7 @@ const { } = 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'); router.post('/abort', handleAbort()); @@ -28,7 +29,7 @@ router.post( buildEndpointOption, setHeaders, async (req, res, next) => { - await AgentController(req, res, next, initializeClient); + await AgentController(req, res, next, initializeClient, addTitle); }, ); diff --git a/api/server/routes/agents/v1.js b/api/server/routes/agents/v1.js index d3a3005bd5..8e347bbe2a 100644 --- a/api/server/routes/agents/v1.js +++ b/api/server/routes/agents/v1.js @@ -2,6 +2,7 @@ const multer = require('multer'); const express = require('express'); const { PermissionTypes, Permissions } = require('librechat-data-provider'); const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware'); +const { getAvailableTools } = require('~/server/controllers/PluginController'); const v1 = require('~/server/controllers/agents/v1'); const actions = require('./actions'); @@ -36,9 +37,7 @@ router.use('/actions', actions); * @route GET /agents/tools * @returns {TPlugin[]} 200 - application/json */ -router.use('/tools', (req, res) => { - res.json([]); -}); +router.use('/tools', getAvailableTools); /** * Creates an agent. diff --git a/api/server/routes/bedrock/chat.js b/api/server/routes/bedrock/chat.js index 605a012710..c8d6be35de 100644 --- a/api/server/routes/bedrock/chat.js +++ b/api/server/routes/bedrock/chat.js @@ -10,7 +10,7 @@ const { } = require('~/server/middleware'); const { initializeClient } = require('~/server/services/Endpoints/bedrock'); const AgentController = require('~/server/controllers/agents/request'); -const addTitle = require('~/server/services/Endpoints/bedrock/title'); +const addTitle = require('~/server/services/Endpoints/agents/title'); router.post('/abort', handleAbort()); diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index 1804244ae5..dc6c99b29b 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -1,18 +1,22 @@ const fs = require('fs').promises; const express = require('express'); +const { EnvVar } = require('@librechat/agents'); const { isUUID, - checkOpenAIStorage, FileSources, EModelEndpoint, + isAgentsEndpoint, + checkOpenAIStorage, } = require('librechat-data-provider'); const { filterFile, processFileUpload, processDeleteRequest, + processAgentFileUpload, } = require('~/server/services/Files/process'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); +const { loadAuthValues } = require('~/app/clients/tools/util'); const { getFiles } = require('~/models/File'); const { logger } = require('~/config'); @@ -64,6 +68,11 @@ router.delete('/', async (req, res) => { await processDeleteRequest({ req, files }); + logger.debug( + `[/files] Files deleted successfully: ${files.map( + (f, i) => `${f.file_id}${i < files.length - 1 ? ', ' : ''}`, + )}`, + ); res.status(200).json({ message: 'Files deleted successfully' }); } catch (error) { logger.error('[/files] Error deleting files:', error); @@ -71,6 +80,36 @@ router.delete('/', async (req, res) => { } }); +router.get('/code/download/:sessionId/:fileId', async (req, res) => { + try { + const { sessionId, fileId } = req.params; + const logPrefix = `Session ID: ${sessionId} | File ID: ${fileId} | Code output download requested by user `; + logger.debug(logPrefix); + + if (!sessionId || !fileId) { + return res.status(400).send('Bad request'); + } + + const { getDownloadStream } = getStrategyFunctions(FileSources.execute_code); + if (!getDownloadStream) { + logger.warn( + `${logPrefix} has no stream method implemented for ${FileSources.execute_code} source`, + ); + return res.status(501).send('Not Implemented'); + } + + const result = await loadAuthValues({ userId: req.user.id, authFields: [EnvVar.CODE_API_KEY] }); + + /** @type {AxiosResponse | undefined} */ + const response = await getDownloadStream(`${sessionId}/${fileId}`, result[EnvVar.CODE_API_KEY]); + res.set(response.headers); + response.data.pipe(res); + } catch (error) { + logger.error('Error downloading file:', error); + res.status(500).send('Error downloading file'); + } +}); + router.get('/download/:userId/:file_id', async (req, res) => { try { const { userId, file_id } = req.params; @@ -154,6 +193,10 @@ router.post('/', async (req, res) => { metadata.temp_file_id = metadata.file_id; metadata.file_id = req.file_id; + if (isAgentsEndpoint(metadata.endpoint)) { + return await processAgentFileUpload({ req, res, file, metadata }); + } + await processFileUpload({ req, res, file, metadata }); } catch (error) { let message = 'Error processing file'; diff --git a/api/server/services/Config/EndpointService.js b/api/server/services/Config/EndpointService.js index 56768905b8..49f9d8f548 100644 --- a/api/server/services/Config/EndpointService.js +++ b/api/server/services/Config/EndpointService.js @@ -49,6 +49,10 @@ 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.I_AM_A_TEAPOT), + [EModelEndpoint.agents]: generateConfig( + process.env.EXPERIMENTAL_AGENTS, + undefined, + EModelEndpoint.agents, + ), }, }; diff --git a/api/server/services/Endpoints/agents/build.js b/api/server/services/Endpoints/agents/build.js index d04dee9a06..853c9ba266 100644 --- a/api/server/services/Endpoints/agents/build.js +++ b/api/server/services/Endpoints/agents/build.js @@ -1,13 +1,12 @@ -const { getAgent } = require('~/models/Agent'); +const { loadAgent } = require('~/models/Agent'); const { logger } = require('~/config'); const buildOptions = (req, endpoint, parsedBody) => { const { agent_id, instructions, spec, ...model_parameters } = parsedBody; - const agentPromise = getAgent({ - id: agent_id, - // TODO: better author handling - author: req.user.id, + const agentPromise = loadAgent({ + req, + agent_id, }).catch((error) => { logger.error(`[/agents/:${agent_id}] Error retrieving agent during build options step`, error); return undefined; diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index a079e2145f..d01d8388a0 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -14,14 +14,16 @@ const { tool } = require('@langchain/core/tools'); const { createContentAggregator } = require('@librechat/agents'); const { EModelEndpoint, - providerEndpointMap, getResponseSender, + providerEndpointMap, } = require('librechat-data-provider'); -const { getDefaultHandlers } = require('~/server/controllers/agents/callbacks'); -// for testing purposes -// const createTavilySearchTool = require('~/app/clients/tools/structured/TavilySearch'); +const { + getDefaultHandlers, + createToolEndCallback, +} = require('~/server/controllers/agents/callbacks'); const initAnthropic = require('~/server/services/Endpoints/anthropic/initializeClient'); const initOpenAI = require('~/server/services/Endpoints/openAI/initializeClient'); +const getBedrockOptions = require('~/server/services/Endpoints/bedrock/options'); const { loadAgentTools } = require('~/server/services/ToolService'); const AgentClient = require('~/server/controllers/agents/client'); const { getModelMaxTokens } = require('~/utils'); @@ -50,6 +52,7 @@ const providerConfigMap = { [EModelEndpoint.openAI]: initOpenAI, [EModelEndpoint.azureOpenAI]: initOpenAI, [EModelEndpoint.anthropic]: initAnthropic, + [EModelEndpoint.bedrock]: getBedrockOptions, }; const initializeClient = async ({ req, res, endpointOption }) => { @@ -58,34 +61,33 @@ const initializeClient = async ({ req, res, endpointOption }) => { } // TODO: use endpointOption to determine options/modelOptions + /** @type {Array} */ + const collectedUsage = []; + /** @type {ArtifactPromises} */ + const artifactPromises = []; const { contentParts, aggregateContent } = createContentAggregator(); - const eventHandlers = getDefaultHandlers({ res, aggregateContent }); - - // const tools = [createTavilySearchTool()]; - // const tools = [_getWeather]; - // const tool_calls = [{ name: 'getPeople_action_swapi---dev' }]; - // const tool_calls = [{ name: 'dalle' }]; - // const tool_calls = [{ name: 'getItmOptions_action_YWlhcGkzLn' }]; - // const tool_calls = [{ name: 'tavily_search_results_json' }]; - // const tool_calls = [ - // { name: 'searchListings_action_emlsbG93NT' }, - // { name: 'searchAddress_action_emlsbG93NT' }, - // { name: 'searchMLS_action_emlsbG93NT' }, - // { name: 'searchCoordinates_action_emlsbG93NT' }, - // { name: 'searchUrl_action_emlsbG93NT' }, - // { name: 'getPropertyDetails_action_emlsbG93NT' }, - // ]; + const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); + const eventHandlers = getDefaultHandlers({ + res, + aggregateContent, + toolEndCallback, + collectedUsage, + }); if (!endpointOption.agent) { throw new Error('No agent promise provided'); } - /** @type {Agent} */ + /** @type {Agent | null} */ const agent = await endpointOption.agent; + if (!agent) { + throw new Error('Agent not found'); + } const { tools, toolMap } = await loadAgentTools({ req, tools: agent.tools, agent_id: agent.id, + tool_resources: agent.tool_resources, // openAIApiKey: process.env.OPENAI_API_KEY, }); @@ -121,8 +123,11 @@ const initializeClient = async ({ req, res, endpointOption }) => { contentParts, modelOptions, eventHandlers, + collectedUsage, + artifactPromises, endpoint: EModelEndpoint.agents, configOptions: options.configOptions, + attachments: endpointOption.attachments, maxContextTokens: agent.max_context_tokens ?? getModelMaxTokens(modelOptions.model, providerEndpointMap[agent.provider]), diff --git a/api/server/services/Endpoints/bedrock/title.js b/api/server/services/Endpoints/agents/title.js similarity index 93% rename from api/server/services/Endpoints/bedrock/title.js rename to api/server/services/Endpoints/agents/title.js index 520b9f78c4..56fd28668d 100644 --- a/api/server/services/Endpoints/bedrock/title.js +++ b/api/server/services/Endpoints/agents/title.js @@ -33,7 +33,7 @@ const addTitle = async (req, { text, response, client }) => { conversationId: response.conversationId, title, }, - { context: 'api/server/services/Endpoints/bedrock/title.js' }, + { context: 'api/server/services/Endpoints/agents/title.js' }, ); }; diff --git a/api/server/services/Files/Code/crud.js b/api/server/services/Files/Code/crud.js new file mode 100644 index 0000000000..10aa130d2e --- /dev/null +++ b/api/server/services/Files/Code/crud.js @@ -0,0 +1,34 @@ +// downloadStream.js + +const axios = require('axios'); +const { getCodeBaseURL } = require('@librechat/agents'); + +const baseURL = getCodeBaseURL(); + +/** + * Retrieves a download stream for a specified file. + * @param {string} fileIdentifier - The identifier for the file (e.g., "sessionId/fileId"). + * @param {string} apiKey - The API key for authentication. + * @returns {Promise} A promise that resolves to a readable stream of the file content. + * @throws {Error} If there's an error during the download process. + */ +async function getCodeOutputDownloadStream(fileIdentifier, apiKey) { + try { + const response = await axios({ + method: 'get', + url: `${baseURL}/download/${fileIdentifier}`, + responseType: 'stream', + headers: { + 'User-Agent': 'LibreChat/1.0', + 'X-API-Key': apiKey, + }, + timeout: 15000, + }); + + return response; + } catch (error) { + throw new Error(`Error downloading file: ${error.message}`); + } +} + +module.exports = { getCodeOutputDownloadStream }; diff --git a/api/server/services/Files/Code/index.js b/api/server/services/Files/Code/index.js new file mode 100644 index 0000000000..a6223d1ee5 --- /dev/null +++ b/api/server/services/Files/Code/index.js @@ -0,0 +1,5 @@ +const crud = require('./crud'); + +module.exports = { + ...crud, +}; diff --git a/api/server/services/Files/Code/process.js b/api/server/services/Files/Code/process.js new file mode 100644 index 0000000000..d8e8119367 --- /dev/null +++ b/api/server/services/Files/Code/process.js @@ -0,0 +1,87 @@ +const path = require('path'); +const { v4 } = require('uuid'); +const axios = require('axios'); +const { getCodeBaseURL, EnvVar } = require('@librechat/agents'); +const { FileContext, imageExtRegex } = require('librechat-data-provider'); +const { convertImage } = require('~/server/services/Files/images/convert'); +const { loadAuthValues } = require('~/app/clients/tools/util'); +const { createFile } = require('~/models/File'); +const { logger } = require('~/config'); + +/** + * Process OpenAI image files, convert to target format, save and return file metadata. + * @param {ServerRequest} params.req - The Express request object. + * @param {string} params.id - The file ID. + * @param {string} params.name - The filename. + * @param {string} params.toolCallId - The tool call ID that generated the file. + * @param {string} params.sessionId - The code execution session ID. + * @param {string} params.conversationId - The current conversation ID. + * @param {string} params.messageId - The current message ID. + * @returns {Promise} The file metadata or undefined if an error occurs. + */ +const processCodeOutput = async ({ + req, + id, + name, + toolCallId, + conversationId, + messageId, + sessionId, +}) => { + const currentDate = new Date(); + const baseURL = getCodeBaseURL(); + const fileExt = path.extname(name); + if (!fileExt || !imageExtRegex.test(name)) { + return { + filename: name, + filepath: `/api/files/code/download/${sessionId}/${id}`, + /** Note: expires 24 hours after creation */ + expiresAt: currentDate.getTime() + 86400000, + conversationId, + toolCallId, + messageId, + }; + } + + try { + const formattedDate = currentDate.toISOString(); + const result = await loadAuthValues({ userId: req.user.id, authFields: [EnvVar.CODE_API_KEY] }); + const response = await axios({ + method: 'get', + url: `${baseURL}/download/${sessionId}/${id}`, + responseType: 'arraybuffer', + headers: { + 'User-Agent': 'LibreChat/1.0', + 'X-API-Key': result[EnvVar.CODE_API_KEY], + }, + timeout: 15000, + }); + + const buffer = Buffer.from(response.data, 'binary'); + + const file_id = v4(); + const _file = await convertImage(req, buffer, 'high', `${file_id}${fileExt}`); + const file = { + ..._file, + file_id, + usage: 1, + filename: name, + conversationId, + user: req.user.id, + type: `image/${req.app.locals.imageOutputType}`, + createdAt: formattedDate, + updatedAt: formattedDate, + source: req.app.locals.fileStrategy, + context: FileContext.execute_code, + }; + createFile(file, true); + /** Note: `messageId` & `toolCallId` are not part of file DB schema; message object records associated file ID */ + return Object.assign(file, { messageId, toolCallId }); + } catch (error) { + logger.error('Error downloading file:', error); + } +}; + +module.exports = { + processCodeOutput, +}; diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index 197fd160cf..00d98bbc20 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -8,6 +8,7 @@ const { FileSources, imageExtRegex, EModelEndpoint, + EToolResources, mergeFileConfig, hostImageIdSuffix, checkOpenAIStorage, @@ -16,6 +17,7 @@ const { } = require('librechat-data-provider'); const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2'); const { convertImage, resizeAndConvert } = require('~/server/services/Files/images'); +const { addAgentResourceFile, removeAgentResourceFile } = require('~/models/Agent'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { createFile, updateFileUsage, deleteFiles } = require('~/models/File'); const { LB_QueueAsyncCall } = require('~/server/utils/queue'); @@ -124,6 +126,17 @@ const processDeleteRequest = async ({ req, files }) => { 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, + }), + ); + } + if (checkOpenAIStorage(source) && !client[source]) { await initializeClients(); } @@ -398,6 +411,95 @@ const processFileUpload = async ({ req, res, file, metadata }) => { res.status(200).json({ message: 'File uploaded and processed successfully', ...result }); }; +/** + * Applies the current strategy for file uploads. + * Saves file metadata to the database with an expiry TTL. + * 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 {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} + */ +const processAgentFileUpload = async ({ req, res, file, metadata }) => { + const { agent_id, tool_resource } = metadata; + if (agent_id && !tool_resource) { + throw new Error('No tool resource provided for agent file upload'); + } + + if (tool_resource === EToolResources.file_search && file.mimetype.startsWith('image')) { + throw new Error('Image uploads are not supported for file search tool resources'); + } + + let messageAttachment = !!metadata.message_file; + if (!messageAttachment && !agent_id) { + throw new Error('No agent ID provided for agent file upload'); + } + + const source = + tool_resource === EToolResources.file_search + ? FileSources.vectordb + : req.app.locals.fileStrategy; + const { handleFileUpload } = getStrategyFunctions(source); + const { file_id, temp_file_id } = metadata; + + const { + bytes, + filename, + filepath: _filepath, + embedded, + height, + width, + } = await handleFileUpload({ + req, + file, + file_id, + }); + + let filepath = _filepath; + + if (!messageAttachment && tool_resource) { + await addAgentResourceFile({ + req, + agent_id, + file_id, + tool_resource: tool_resource, + }); + } + + if (file.mimetype.startsWith('image')) { + const result = await processImageFile({ + req, + file, + metadata: { file_id: v4() }, + returnFile: true, + }); + filepath = result.filepath; + } + + const result = await createFile( + { + user: req.user.id, + file_id, + temp_file_id, + bytes, + filepath, + filename: filename ?? file.originalname, + context: messageAttachment ? FileContext.message_attachment : FileContext.agents, + model: messageAttachment ? undefined : req.body.model, + type: file.mimetype, + embedded, + source, + height, + width, + }, + true, + ); + res.status(200).json({ message: 'Agent file uploaded and processed successfully', ...result }); +}; + /** * @param {object} params - The params object. * @param {OpenAI} params.openai - The OpenAI client instance. @@ -654,5 +756,6 @@ module.exports = { uploadImageBuffer, processFileUpload, processDeleteRequest, + processAgentFileUpload, retrieveAndProcessFile, }; diff --git a/api/server/services/Files/strategies.js b/api/server/services/Files/strategies.js index fa4e456fc9..cfb837a50e 100644 --- a/api/server/services/Files/strategies.js +++ b/api/server/services/Files/strategies.js @@ -21,6 +21,7 @@ const { } = require('./Local'); const { uploadOpenAIFile, deleteOpenAIFile, getOpenAIFileStream } = require('./OpenAI'); const { uploadVectors, deleteVectors } = require('./VectorDB'); +const { getCodeOutputDownloadStream } = require('./Code'); /** * Firebase Storage Strategy Functions @@ -103,6 +104,31 @@ const openAIStrategy = () => ({ getDownloadStream: getOpenAIFileStream, }); +/** + * Code Output Strategy Functions + * + * Note: null values mean that the strategy is not supported. + * */ +const codeOutputStrategy = () => ({ + /** @type {typeof saveFileFromURL | null} */ + saveURL: null, + /** @type {typeof getLocalFileURL | null} */ + getFileURL: null, + /** @type {typeof saveLocalBuffer | null} */ + saveBuffer: null, + /** @type {typeof processLocalAvatar | null} */ + processAvatar: null, + /** @type {typeof uploadLocalImage | null} */ + handleImageUpload: null, + /** @type {typeof prepareImagesLocal | null} */ + prepareImagePayload: null, + /** @type {typeof deleteLocalFile | null} */ + deleteFile: null, + /** @type {typeof uploadVectors | null} */ + handleFileUpload: null, + getDownloadStream: getCodeOutputDownloadStream, +}); + // Strategy Selector const getStrategyFunctions = (fileSource) => { if (fileSource === FileSources.firebase) { @@ -115,6 +141,8 @@ const getStrategyFunctions = (fileSource) => { return openAIStrategy(); } else if (fileSource === FileSources.vectordb) { return vectorStrategy(); + } else if (fileSource === FileSources.execute_code) { + return codeOutputStrategy(); } else { throw new Error('Invalid file source'); } diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 109fbeddea..d502409593 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -1,9 +1,8 @@ const fs = require('fs'); const path = require('path'); -const { StructuredTool } = require('langchain/tools'); -const { tool: toolFn } = require('@langchain/core/tools'); const { zodToJsonSchema } = require('zod-to-json-schema'); const { Calculator } = require('langchain/tools/calculator'); +const { tool: toolFn, Tool } = require('@langchain/core/tools'); const { Tools, ContentTypes, @@ -70,7 +69,7 @@ function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] }) continue; } - if (!ToolClass || !(ToolClass.prototype instanceof StructuredTool)) { + if (!ToolClass || !(ToolClass.prototype instanceof Tool)) { continue; } @@ -378,11 +377,12 @@ async function processRequiredActions(client, requiredActions) { * @param {Object} params - Run params containing user and request information. * @param {ServerRequest} params.req - The request object. * @param {string} params.agent_id - The agent ID. - * @param {string[]} params.tools - The agent's available tools. + * @param {Agent['tools']} params.tools - The agent's available tools. + * @param {Agent['tool_resources']} params.tool_resources - The agent's available tool resources. * @param {string | undefined} [params.openAIApiKey] - The OpenAI API key. * @returns {Promise<{ tools?: StructuredTool[]; toolMap?: Record}>} The combined toolMap. */ -async function loadAgentTools({ req, agent_id, tools, openAIApiKey }) { +async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiKey }) { if (!tools || tools.length === 0) { return {}; } @@ -394,6 +394,7 @@ async function loadAgentTools({ req, agent_id, tools, openAIApiKey }) { options: { req, openAIApiKey, + tool_resources, returnMetadata: true, processFileURL, uploadImageBuffer, @@ -405,6 +406,10 @@ async function loadAgentTools({ req, agent_id, tools, openAIApiKey }) { const agentTools = []; for (let i = 0; i < loadedTools.length; i++) { const tool = loadedTools[i]; + if (tool.name && (tool.name === Tools.execute_code || tool.name === Tools.file_search)) { + agentTools.push(tool); + continue; + } const toolInstance = toolFn( async (...args) => { diff --git a/api/server/utils/handleText.js b/api/server/utils/handleText.js index 17ab569726..2668569834 100644 --- a/api/server/utils/handleText.js +++ b/api/server/utils/handleText.js @@ -1,6 +1,8 @@ const { Capabilities, EModelEndpoint, + isAgentsEndpoint, + AgentCapabilities, isAssistantsEndpoint, defaultRetrievalModels, defaultAssistantsVersion, @@ -160,8 +162,8 @@ const isUserProvided = (value) => value === 'user_provided'; /** * Generate the configuration for a given key and base URL. * @param {string} key - * @param {string} baseURL - * @param {string} endpoint + * @param {string} [baseURL] + * @param {string} [endpoint] * @returns {boolean | { userProvide: boolean, userProvideURL?: boolean }} */ function generateConfig(key, baseURL, endpoint) { @@ -177,7 +179,7 @@ function generateConfig(key, baseURL, endpoint) { } const assistants = isAssistantsEndpoint(endpoint); - + const agents = isAgentsEndpoint(endpoint); if (assistants) { config.retrievalModels = defaultRetrievalModels; config.capabilities = [ @@ -189,6 +191,18 @@ function generateConfig(key, baseURL, endpoint) { ]; } + if (agents) { + config.capabilities = [ + AgentCapabilities.file_search, + AgentCapabilities.actions, + AgentCapabilities.tools, + ]; + + if (key === 'EXPERIMENTAL_RUN_CODE') { + config.capabilities.push(AgentCapabilities.execute_code); + } + } + if (assistants && endpoint === EModelEndpoint.azureAssistants) { config.version = defaultAssistantsVersion.azureAssistants; } else if (assistants) { diff --git a/api/typedefs.js b/api/typedefs.js index 163768f58a..8f0e9fef25 100644 --- a/api/typedefs.js +++ b/api/typedefs.js @@ -646,9 +646,22 @@ * @property {string} [temp_file_id] - The temporary identifier of the file. * @property {string} endpoint - The conversation endpoint origin for the file upload. * @property {string} [assistant_id] - The assistant ID if file upload is in the `knowledge` context. + * @property {string} [tool_resource] - The relevant tool resource for the file upload. * @memberof typedefs */ +/** + * @exports FileObject + * @typedef {{file_id: string, filepath: string, source: string, bytes?: number, width?: number, height?: number}} FileObject + * @memberof typedefs + * + +/** + * @exports ArtifactPromises + * @typedef {Promise[]} ArtifactPromises + * @memberof typedefs + * + /** * @typedef {Object} ImageOnlyMetadata * @property {number} width - The width of the image. @@ -706,6 +719,12 @@ * @memberof typedefs */ +/** + * @exports TAttachment + * @typedef {import('librechat-data-provider').TAttachment} TAttachment + * @memberof typedefs + */ + /** * @exports AssistantCreateParams * @typedef {import('librechat-data-provider').AssistantCreateParams} AssistantCreateParams diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts index 07633a68db..fd268e8cb7 100644 --- a/client/src/common/agents-types.ts +++ b/client/src/common/agents-types.ts @@ -1,17 +1,16 @@ -import { Capabilities } from 'librechat-data-provider'; +import { AgentCapabilities } from 'librechat-data-provider'; import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider'; import type { OptionWithIcon, ExtendedFile } from './types'; export type TAgentOption = OptionWithIcon & Agent & { - files?: Array<[string, ExtendedFile]>; + knowledge_files?: Array<[string, ExtendedFile]>; code_files?: Array<[string, ExtendedFile]>; }; -export type AgentCapabilities = { - [Capabilities.code_interpreter]: boolean; - [Capabilities.image_vision]: boolean; - [Capabilities.retrieval]: boolean; +export type TAgentCapabilities = { + [AgentCapabilities.execute_code]: boolean; + [AgentCapabilities.file_search]: boolean; }; export type AgentForm = { @@ -24,4 +23,4 @@ export type AgentForm = { model_parameters: AgentModelParameters; tools?: string[]; provider?: AgentProvider | OptionWithIcon; -} & AgentCapabilities; +} & TAgentCapabilities; diff --git a/client/src/common/types.ts b/client/src/common/types.ts index 461dfcbe48..3590b279b8 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -194,6 +194,7 @@ export type AgentModelPanelProps = { setActivePanel: React.Dispatch>; providers: Option[]; models: Record; + agent_id?: string; }; export type AugmentedColumnDef = ColumnDef & DataColumnMeta; @@ -430,6 +431,8 @@ export type Option = Record & { value: string | number | null; }; +export type StringOption = Option & { value: string | null }; + export type VoiceOption = { value: string; label: string; @@ -545,4 +548,4 @@ declare global { interface Window { google_tag_manager?: unknown; } -} \ No newline at end of file +} diff --git a/client/src/components/Chat/Input/Files/FileRow.tsx b/client/src/components/Chat/Input/Files/FileRow.tsx index 80bf24b10b..caf62b05dc 100644 --- a/client/src/components/Chat/Input/Files/FileRow.tsx +++ b/client/src/components/Chat/Input/Files/FileRow.tsx @@ -11,14 +11,13 @@ export default function FileRow({ setFiles, setFilesLoading, assistant_id, - // TODO: Agent file handling agent_id, tool_resource, fileFilter, - isRTL, + isRTL = false, Wrapper, }: { - files: Map; + files: Map | undefined; setFiles: React.Dispatch>>; setFilesLoading: React.Dispatch>; fileFilter?: (file: ExtendedFile) => boolean; @@ -28,13 +27,18 @@ export default function FileRow({ isRTL?: boolean; Wrapper?: React.FC<{ children: React.ReactNode }>; }) { - const files = Array.from(_files.values()).filter((file) => + const files = Array.from(_files?.values() ?? []).filter((file) => fileFilter ? fileFilter(file) : true, ); const { mutateAsync } = useDeleteFilesMutation({ onMutate: async () => - console.log('Deleting files: assistant_id, tool_resource', assistant_id, tool_resource), + console.log( + 'Deleting files: agent_id, assistant_id, tool_resource', + agent_id, + assistant_id, + tool_resource, + ), onSuccess: () => { console.log('Files deleted'); }, @@ -43,13 +47,9 @@ export default function FileRow({ }, }); - const { deleteFile } = useFileDeletion({ mutateAsync, assistant_id, tool_resource }); + const { deleteFile } = useFileDeletion({ mutateAsync, agent_id, assistant_id, tool_resource }); useEffect(() => { - if (!files) { - return; - } - if (files.length === 0) { return; } @@ -87,11 +87,12 @@ export default function FileRow({ ) .uniqueFiles.map((file: ExtendedFile, index: number) => { const handleDelete = () => deleteFile({ file, setFiles }); - if (file.type?.startsWith('image')) { + const isImage = file.type?.startsWith('image') ?? false; + if (isImage) { return ( new Map(data.map((dbA) => [dbA.assistant_id, dbA])), }); - const isAssistant = isAssistantsEndpoint(endpoint); - const assistant = isAssistant ? assistantMap?.[endpoint][assistant_id ?? ''] : undefined; - const assistantName = assistant?.name ?? ''; - const assistantDesc = assistant?.description ?? ''; - const avatar = assistant?.metadata?.avatar ?? ''; + const { entity, isAgent, isAssistant } = getEntity({ + endpoint, + agentsMap, + assistantMap, + agent_id: conversation?.agent_id, + assistant_id: conversation?.assistant_id, + }); + + const name = entity?.name ?? ''; + const description = entity?.description ?? ''; + const avatar = isAgent + ? (entity as t.Agent | undefined)?.avatar?.filepath ?? '' + : ((entity as t.Assistant | undefined)?.metadata?.avatar as string | undefined) ?? ''; const conversation_starters = useMemo(() => { - /* The user made updates, use client-side cache, */ - if (assistant?.conversation_starters) { - return assistant.conversation_starters; + /* The user made updates, use client-side cache, or they exist in an Agent */ + if (entity && (entity.conversation_starters?.length ?? 0) > 0) { + return entity.conversation_starters; } + if (isAgent) { + return entity?.conversation_starters ?? []; + } + /* If none in cache, we use the latest assistant docs */ - const assistantDocs = documentsMap.get(assistant_id ?? ''); - return assistantDocs?.conversation_starters ?? []; - }, [documentsMap, assistant_id, assistant?.conversation_starters]); + const entityDocs = documentsMap.get(entity?.id ?? ''); + return entityDocs?.conversation_starters ?? []; + }, [documentsMap, isAgent, entity]); const containerClassName = 'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black'; @@ -57,14 +70,32 @@ export default function Landing({ Header }: { Header?: ReactNode }) { const { submitMessage } = useSubmitMessage(); const sendConversationStarter = (text: string) => submitMessage({ text }); + const getWelcomeMessage = () => { + const greeting = conversation?.greeting ?? ''; + if (greeting) { + return greeting; + } + + if (isAssistant) { + return localize('com_nav_welcome_assistant'); + } + + if (isAgent) { + return localize('com_nav_welcome_agent'); + } + + return localize('com_nav_welcome_message'); + }; + return (
{Header != null ? Header : null}
-
+
) : null}
- {assistantName ? ( + {name ? (
-
{assistantName}
+
{name}
- {assistantDesc ? assistantDesc : localize('com_nav_welcome_message')} + {description ? description : localize('com_nav_welcome_message')}
{/*
By Daniel Avila
@@ -92,16 +123,14 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
) : (

- {isAssistant - ? conversation?.greeting ?? localize('com_nav_welcome_assistant') - : conversation?.greeting ?? localize('com_nav_welcome_message')} + {getWelcomeMessage()}

)}
{conversation_starters.length > 0 && conversation_starters .slice(0, Constants.MAX_CONVO_STARTERS) - .map((text, index) => ( + .map((text: string, index: number) => ( { +const AssistantAvatar = ({ + className = '', + assistantName = '', + avatar = '', + context, + size, +}: IconMapProps) => { if (assistantName && avatar) { return ( ; } - return ; + return ; }; -const AgentAvatar = ({ className = '', agentName, avatar, size }: AgentIconMapProps) => { +const AgentAvatar = ({ className = '', avatar = '', agentName, size }: AgentIconMapProps) => { if (agentName && avatar) { return ( ); - } else if (agentName) { - return ; } - return ; + return ; }; const Bedrock = ({ className = '' }: IconMapProps) => { diff --git a/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx b/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx index 3ee52c5f3f..25bb0e4ab2 100644 --- a/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx +++ b/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx @@ -113,7 +113,7 @@ const MenuItem: FC = ({
- {Icon && ( + {Icon != null && ( { const { data: endpoints = [] } = useGetEndpointsQuery({ select: mapEndpoints, }); - const { conversation } = useChatContext(); - const { endpoint = '', assistant_id = null } = conversation ?? {}; + const agentsMap = useAgentsMapContext(); const assistantMap = useAssistantsMapContext(); - - const assistant = - isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint ?? '']?.[assistant_id ?? '']; - const assistantName = (assistant && assistant?.name) || 'Assistant'; + const { conversation } = useChatContext(); + const { endpoint = '' } = conversation ?? {}; if (!endpoint) { console.warn('No endpoint selected'); return null; } - const primaryText = assistant ? assistantName : (alternateName[endpoint] ?? endpoint ?? '') + ' '; + const { entity } = getEntity({ + endpoint, + agentsMap, + assistantMap, + agent_id: conversation?.agent_id, + assistant_id: conversation?.assistant_id, + }); + + const primaryText = entity + ? entity.name + : (alternateName[endpoint] as string | undefined) ?? endpoint; return ( @@ -44,7 +51,7 @@ const EndpointsMenu: FC = () => { diff --git a/client/src/components/Chat/Messages/Content/CodeAnalyze.tsx b/client/src/components/Chat/Messages/Content/CodeAnalyze.tsx index 96bcca9a4b..d062f4276a 100644 --- a/client/src/components/Chat/Messages/Content/CodeAnalyze.tsx +++ b/client/src/components/Chat/Messages/Content/CodeAnalyze.tsx @@ -1,11 +1,10 @@ import { useState } from 'react'; import { useRecoilValue } from 'recoil'; -import ProgressCircle from './ProgressCircle'; -import CancelledIcon from './CancelledIcon'; +import { CodeInProgress } from './Parts/CodeProgress'; +import { useProgress, useLocalize } from '~/hooks'; import ProgressText from './ProgressText'; import FinishedIcon from './FinishedIcon'; import MarkdownLite from './MarkdownLite'; -import { useProgress } from '~/hooks'; import store from '~/store'; export default function CodeAnalyze({ @@ -19,9 +18,11 @@ export default function CodeAnalyze({ outputs: Record[]; isSubmitting: boolean; }) { - const showCodeDefault = useRecoilValue(store.showCode); - const [showCode, setShowCode] = useState(showCodeDefault); + const localize = useLocalize(); const progress = useProgress(initialProgress); + const showAnalysisCode = useRecoilValue(store.showCode); + const [showCode, setShowCode] = useState(showAnalysisCode); + const radius = 56.08695652173913; const circumference = 2 * Math.PI * radius; const offset = circumference - progress * circumference; @@ -62,7 +63,7 @@ export default function CodeAnalyze({ {logs && (
-
Result
+
{localize('com_ui_result')}
); } - -const CodeInProgress = ({ - offset, - circumference, - radius, - isSubmitting, - progress, -}: { - progress: number; - offset: number; - circumference: number; - radius: number; - isSubmitting: boolean; -}) => { - if (progress < 1 && !isSubmitting) { - return ; - } - return ( -
-
- - - - - - - - - - - - - - - - - - - - - -
- -
- ); -}; diff --git a/client/src/components/Chat/Messages/Content/ContentParts.tsx b/client/src/components/Chat/Messages/Content/ContentParts.tsx index b64a289dd2..c2018d1905 100644 --- a/client/src/components/Chat/Messages/Content/ContentParts.tsx +++ b/client/src/components/Chat/Messages/Content/ContentParts.tsx @@ -1,12 +1,16 @@ -import { memo } from 'react'; +import { memo, useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; import { ContentTypes } from 'librechat-data-provider'; -import type { TMessageContentParts } from 'librechat-data-provider'; +import type { TMessageContentParts, TAttachment, Agents } from 'librechat-data-provider'; import EditTextPart from './Parts/EditTextPart'; +import { mapAttachments } from '~/utils/map'; +import store from '~/store'; import Part from './Part'; type ContentPartsProps = { content: Array | undefined; messageId: string; + attachments?: TAttachment[]; isCreatedByUser: boolean; isLast: boolean; isSubmitting: boolean; @@ -23,6 +27,7 @@ const ContentParts = memo( ({ content, messageId, + attachments, isCreatedByUser, isLast, isSubmitting, @@ -31,6 +36,11 @@ const ContentParts = memo( siblingIdx, setSiblingIdx, }: ContentPartsProps) => { + const messageAttachmentsMap = useRecoilValue(store.messageAttachmentsMap); + const attachmentMap = useMemo( + () => mapAttachments(attachments ?? messageAttachmentsMap[messageId] ?? []), + [attachments, messageAttachmentsMap, messageId], + ); if (!content) { return null; } @@ -58,20 +68,28 @@ const ContentParts = memo( ); } + return ( <> {content .filter((part) => part) - .map((part, idx) => ( - - ))} + .map((part, idx) => { + const toolCallId = + (part?.[ContentTypes.TOOL_CALL] as Agents.ToolCall | undefined)?.id ?? ''; + const attachments = attachmentMap[toolCallId]; + + return ( + + ); + })} ); }, diff --git a/client/src/components/Chat/Messages/Content/Part.tsx b/client/src/components/Chat/Messages/Content/Part.tsx index ca0469cbfd..83241a4211 100644 --- a/client/src/components/Chat/Messages/Content/Part.tsx +++ b/client/src/components/Chat/Messages/Content/Part.tsx @@ -1,12 +1,14 @@ import { - ToolCallTypes, + Tools, ContentTypes, + ToolCallTypes, imageGenTools, isImageVisionTool, } from 'librechat-data-provider'; import { memo } from 'react'; -import type { TMessageContentParts } from 'librechat-data-provider'; +import type { TMessageContentParts, TAttachment } from 'librechat-data-provider'; import { ErrorMessage } from './MessageContent'; +import ExecuteCode from './Parts/ExecuteCode'; import RetrievalCall from './RetrievalCall'; import CodeAnalyze from './CodeAnalyze'; import Container from './Container'; @@ -21,125 +23,141 @@ type PartProps = { showCursor: boolean; messageId: string; isCreatedByUser: boolean; + attachments?: TAttachment[]; }; -const Part = memo(({ part, isSubmitting, showCursor, messageId, 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.TOOL_CALL) { - const toolCall = part[ContentTypes.TOOL_CALL]; - - if (!toolCall) { +const Part = memo( + ({ part, isSubmitting, attachments, showCursor, messageId, isCreatedByUser }: PartProps) => { + attachments && console.log(attachments); + if (!part) { return null; } - if ('args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL)) { + 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 (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 ( - - - - ); - } + } 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/CodeProgress.tsx b/client/src/components/Chat/Messages/Content/Parts/CodeProgress.tsx new file mode 100644 index 0000000000..6e70688cd6 --- /dev/null +++ b/client/src/components/Chat/Messages/Content/Parts/CodeProgress.tsx @@ -0,0 +1,90 @@ +import ProgressCircle from '~/components/Chat/Messages/Content/ProgressCircle'; +import CancelledIcon from '~/components/Chat/Messages/Content/CancelledIcon'; + +export const CodeInProgress = ({ + offset, + circumference, + radius, + isSubmitting, + progress, +}: { + progress: number; + offset: number; + circumference: number; + radius: number; + isSubmitting: boolean; +}) => { + if (progress < 1 && !isSubmitting) { + return ; + } + return ( +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+ ); +}; diff --git a/client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx b/client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx new file mode 100644 index 0000000000..559fc3cd2c --- /dev/null +++ b/client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx @@ -0,0 +1,127 @@ +import React, { useMemo, useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { CodeInProgress } from './CodeProgress'; +import { imageExtRegex } from 'librechat-data-provider'; +import type { TFile, TAttachment, TAttachmentMetadata } from 'librechat-data-provider'; +import ProgressText from '~/components/Chat/Messages/Content/ProgressText'; +import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon'; +import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite'; +import Image from '~/components/Chat/Messages/Content/Image'; +import LogContent from './LogContent'; +import { useProgress } from '~/hooks'; +import store from '~/store'; + +interface ParsedArgs { + lang: string; + code: string; +} + +export function useParseArgs(args: string): ParsedArgs { + return useMemo(() => { + const langMatch = args.match(/"lang"\s*:\s*"(\w+)"/); + const codeMatch = args.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"args"|$)/s); + + let code = ''; + if (codeMatch) { + code = codeMatch[1]; + if (code.endsWith('"}')) { + code = code.slice(0, -2); + } + code = code.replace(/\\n/g, '\n').replace(/\\/g, ''); + } + + return { + lang: langMatch ? langMatch[1] : '', + code, + }; + }, [args]); +} + +export default function ExecuteCode({ + initialProgress = 0.1, + args, + output = '', + isSubmitting, + attachments, +}: { + initialProgress: number; + args: string; + output?: string; + isSubmitting: boolean; + attachments?: TAttachment[]; +}) { + const showAnalysisCode = useRecoilValue(store.showCode); + const [showCode, setShowCode] = useState(showAnalysisCode); + + const { lang, code } = useParseArgs(args); + const progress = useProgress(initialProgress); + + const radius = 56.08695652173913; + const circumference = 2 * Math.PI * radius; + const offset = circumference - progress * circumference; + + return ( + <> +
+
+ {progress < 1 ? ( + + ) : ( + + )} +
+ setShowCode((prev) => !prev)} + inProgressText="Analyzing" + finishedText="Finished analyzing" + hasInput={!!code.length} + /> +
+ {showCode && ( +
+ + {output.length > 0 && ( +
+
+
+                  
+                
+
+
+ )} +
+ )} + {attachments?.map((attachment, index) => { + const { width, height, filepath } = attachment as TFile & TAttachmentMetadata; + const isImage = + imageExtRegex.test(attachment.filename) && + width != null && + height != null && + filepath != null; + if (isImage) { + return ( + + ); + } + })} + + ); +} diff --git a/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx b/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx new file mode 100644 index 0000000000..9f9eceaa4f --- /dev/null +++ b/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx @@ -0,0 +1,66 @@ +import { isAfter } from 'date-fns'; +import React, { useMemo } from 'react'; +import { imageExtRegex } from 'librechat-data-provider'; +import type { TAttachment } from 'librechat-data-provider'; +import { useLocalize } from '~/hooks'; +import LogLink from './LogLink'; + +interface LogContentProps { + output?: string; + attachments?: TAttachment[]; +} + +const LogContent: React.FC = ({ output = '', attachments }) => { + const localize = useLocalize(); + const processedContent = useMemo(() => { + if (!output) { + return ''; + } + + const parts = output.split('Generated files:'); + return parts[0].trim(); + }, [output]); + + const nonImageAttachments = + attachments?.filter((file) => !imageExtRegex.test(file.filename)) || []; + + const renderAttachment = (file: TAttachment) => { + const now = new Date(); + const expiresAt = typeof file.expiresAt === 'number' ? new Date(file.expiresAt) : null; + const isExpired = expiresAt ? isAfter(now, expiresAt) : false; + + if (isExpired) { + return `${file.filename} ${localize('com_download_expired')}`; + } + + // const expirationText = expiresAt + // ? ` ${localize('com_download_expires', format(expiresAt, 'MM/dd/yy HH:mm'))}` + // : ` ${localize('com_click_to_download')}`; + + return ( + + {'- '} + {file.filename} {localize('com_click_to_download')} + + ); + }; + + return ( + <> + {processedContent &&
{processedContent}
} + {nonImageAttachments.length > 0 && ( +
+

{localize('com_generated_files')}

+ {nonImageAttachments.map((file, index) => ( + + {renderAttachment(file)} + {index < nonImageAttachments.length - 1 && ', '} + + ))} +
+ )} + + ); +}; + +export default LogContent; diff --git a/client/src/components/Chat/Messages/Content/Parts/LogLink.tsx b/client/src/components/Chat/Messages/Content/Parts/LogLink.tsx new file mode 100644 index 0000000000..c1886e7460 --- /dev/null +++ b/client/src/components/Chat/Messages/Content/Parts/LogLink.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { useCodeOutputDownload } from '~/data-provider'; +import { useToastContext } from '~/Providers'; + +interface LogLinkProps { + href: string; + filename: string; + children: React.ReactNode; +} + +const LogLink: React.FC = ({ href, filename, children }) => { + const { showToast } = useToastContext(); + const { refetch: downloadFile } = useCodeOutputDownload(href); + + const handleDownload = async (event: React.MouseEvent) => { + event.preventDefault(); + try { + const stream = await downloadFile(); + if (stream.data == null || stream.data === '') { + console.error('Error downloading file: No data found'); + showToast({ + status: 'error', + message: 'Error downloading file', + }); + return; + } + const link = document.createElement('a'); + link.href = stream.data; + link.setAttribute('download', filename); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(stream.data); + } catch (error) { + console.error('Error downloading file:', error); + } + }; + + return ( + + {children} + + ); +}; + +export default LogLink; diff --git a/client/src/components/Chat/Messages/Content/ToolCall.tsx b/client/src/components/Chat/Messages/Content/ToolCall.tsx index fce15d84a8..d5c7456da0 100644 --- a/client/src/components/Chat/Messages/Content/ToolCall.tsx +++ b/client/src/components/Chat/Messages/Content/ToolCall.tsx @@ -8,7 +8,6 @@ import CancelledIcon from './CancelledIcon'; import ProgressText from './ProgressText'; import FinishedIcon from './FinishedIcon'; import ToolPopover from './ToolPopover'; -// import ActionIcon from './ActionIcon'; import WrenchIcon from './WrenchIcon'; import { useProgress } from '~/hooks'; import { logger } from '~/utils'; @@ -32,7 +31,7 @@ export default function ToolCall({ const circumference = 2 * Math.PI * radius; const offset = circumference - progress * circumference; - const [function_name, _domain] = name.split(actionDelimiter); + const [function_name, _domain] = name.split(actionDelimiter) as [string, string | undefined]; const domain = _domain?.replaceAll(actionDomainSeparator, '.') ?? null; const error = output?.toLowerCase()?.includes('error processing tool'); @@ -50,50 +49,60 @@ export default function ToolCall({ ); return ''; } - }, [_args]); + }, [_args]) as string | undefined; const hasInfo = useMemo( - () => (args?.length || 0) > 0 || (output?.length || 0) > 0, + () => (args?.length ?? 0) > 0 || (output?.length ?? 0) > 0, [args, output], ); + const renderIcon = () => { + if (progress < 1) { + return ( + +
+
+ +
+ +
+
+ ); + } + + return error === true ? : ; + }; + + const getFinishedText = () => { + if (domain != null && domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH) { + return localize('com_assistants_completed_action', domain); + } + return localize('com_assistants_completed_function', function_name); + }; + return (
-
- {progress < 1 ? ( - -
-
- -
- -
-
- ) : error ? ( - - ) : ( - - )} -
+
{renderIcon()}
({})} inProgressText={localize('com_assistants_running_action')} - finishedText={ - domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH - ? localize('com_assistants_completed_action', domain) - : localize('com_assistants_completed_function', function_name) - } + finishedText={getFinishedText()} hasInput={hasInfo} popover={true} /> {hasInfo && ( - + )}
diff --git a/client/src/components/Endpoints/ConvoIcon.tsx b/client/src/components/Endpoints/ConvoIcon.tsx index 4a42e4173a..7e35a99ea6 100644 --- a/client/src/components/Endpoints/ConvoIcon.tsx +++ b/client/src/components/Endpoints/ConvoIcon.tsx @@ -1,12 +1,6 @@ import React, { useMemo } from 'react'; -import { isAssistantsEndpoint } from 'librechat-data-provider'; -import type { - TAssistantsMap, - TConversation, - TEndpointsConfig, - TPreset, -} from 'librechat-data-provider'; -import { getEndpointField, getIconKey, getIconEndpoint } from '~/utils'; +import type * as t from 'librechat-data-provider'; +import { getEndpointField, getIconKey, getEntity, getIconEndpoint } from '~/utils'; import { icons } from '~/components/Chat/Menus/Endpoints/Icons'; import ConvoIconURL from '~/components/Endpoints/ConvoIconURL'; @@ -14,61 +8,72 @@ export default function ConvoIcon({ conversation, endpointsConfig, assistantMap, + agentsMap, className = '', containerClassName = '', context, size, }: { - conversation: TConversation | TPreset | null; - endpointsConfig: TEndpointsConfig; - assistantMap: TAssistantsMap | undefined; + conversation: t.TConversation | t.TPreset | null; + endpointsConfig: t.TEndpointsConfig; + assistantMap: t.TAssistantsMap | undefined; + agentsMap: t.TAgentsMap | undefined; containerClassName?: string; context?: 'message' | 'nav' | 'landing' | 'menu-item'; className?: string; size?: number; }) { - const iconURL = conversation?.iconURL; + const iconURL = conversation?.iconURL ?? ''; let endpoint = conversation?.endpoint; endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint }); - const assistant = useMemo(() => { - if (!isAssistantsEndpoint(conversation?.endpoint)) { - return undefined; - } - const endpointKey = conversation?.endpoint ?? ''; - const assistantId = conversation?.assistant_id ?? ''; + const { entity, isAgent } = useMemo( + () => + getEntity({ + endpoint, + agentsMap, + assistantMap, + agent_id: conversation?.agent_id, + assistant_id: conversation?.assistant_id, + }), + [endpoint, conversation?.agent_id, conversation?.assistant_id, agentsMap, assistantMap], + ); - return assistantMap?.[endpointKey] ? assistantMap[endpointKey][assistantId] : undefined; - }, [conversation?.endpoint, conversation?.assistant_id, assistantMap]); - const assistantName = assistant && (assistant.name ?? ''); + const name = entity?.name ?? ''; + const avatar = isAgent + ? (entity as t.Agent | undefined)?.avatar?.filepath + : ((entity as t.Assistant | undefined)?.metadata?.avatar as string); - const avatar = (assistant && (assistant.metadata?.avatar as string)) || ''; const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL'); const iconKey = getIconKey({ endpoint, endpointsConfig, endpointIconURL }); - const Icon = icons[iconKey]; + const Icon = icons[iconKey] ?? null; + return ( <> {iconURL && iconURL.includes('http') ? ( ) : (
- {endpoint && - Icon && - Icon({ - size, - context, - className, - iconURL: endpointIconURL, - assistantName, - endpoint, - avatar, - })} + {endpoint && Icon != null && ( + + )}
)} diff --git a/client/src/components/Endpoints/ConvoIconURL.tsx b/client/src/components/Endpoints/ConvoIconURL.tsx index 929a2b6200..cfb39b3d1e 100644 --- a/client/src/components/Endpoints/ConvoIconURL.tsx +++ b/client/src/components/Endpoints/ConvoIconURL.tsx @@ -64,7 +64,7 @@ const ConvoIconURL: React.FC = ({
); - return ; + return ; } return ( @@ -73,10 +73,10 @@ const ConvoIconURL: React.FC = ({ size={41} context={context} className="h-2/3 w-2/3" + agentName={agentName} iconURL={endpointIconURL} assistantName={assistantName} - avatar={assistantAvatar || agentAvatar} - agentName={agentName} + avatar={assistantAvatar ?? agentAvatar} />
); diff --git a/client/src/components/Endpoints/MessageEndpointIcon.tsx b/client/src/components/Endpoints/MessageEndpointIcon.tsx index 04c091e7d5..68dc9a88d4 100644 --- a/client/src/components/Endpoints/MessageEndpointIcon.tsx +++ b/client/src/components/Endpoints/MessageEndpointIcon.tsx @@ -1,6 +1,6 @@ import { EModelEndpoint, isAssistantsEndpoint, alternateName } from 'librechat-data-provider'; import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon'; -import { BrainCircuit } from 'lucide-react'; +import { Feather } from 'lucide-react'; import { Plugin, GPTIcon, @@ -109,7 +109,7 @@ const MessageEndpointIcon: React.FC = (props) => { ) : (
- +
), diff --git a/client/src/components/Endpoints/MinimalIcon.tsx b/client/src/components/Endpoints/MinimalIcon.tsx index 4e9a3b28d6..0287fb90b6 100644 --- a/client/src/components/Endpoints/MinimalIcon.tsx +++ b/client/src/components/Endpoints/MinimalIcon.tsx @@ -1,6 +1,5 @@ +import { Feather } from 'lucide-react'; import { EModelEndpoint, alternateName } from 'librechat-data-provider'; -import { BrainCircuit } from 'lucide-react'; -import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon'; import { AzureMinimalIcon, OpenAIMinimalIcon, @@ -13,11 +12,12 @@ import { BedrockIcon, Sparkles, } from '~/components/svg'; -import { cn } from '~/utils'; +import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon'; import { IconProps } from '~/common'; +import { cn } from '~/utils'; const MinimalIcon: React.FC = (props) => { - const { size = 30, iconClassName, error } = props; + const { size = 30, iconURL = '', iconClassName, error } = props; let endpoint = 'default'; // Default value for endpoint @@ -49,7 +49,7 @@ const MinimalIcon: React.FC = (props) => { [EModelEndpoint.assistants]: { icon: , name: 'Assistant' }, [EModelEndpoint.azureAssistants]: { icon: , name: 'Assistant' }, [EModelEndpoint.agents]: { - icon: , + icon: , name: props.modelLabel ?? alternateName[EModelEndpoint.agents], }, [EModelEndpoint.bedrock]: { @@ -57,21 +57,14 @@ const MinimalIcon: React.FC = (props) => { name: props.modelLabel ?? alternateName[EModelEndpoint.bedrock], }, default: { - icon: ( - - ), + icon: , name: endpoint, }, }; let { icon, name } = endpointIcons[endpoint] ?? endpointIcons.default; - if (props.iconURL && endpointIcons[props.iconURL]) { - ({ icon, name } = endpointIcons[props.iconURL]); + if (iconURL && endpointIcons[iconURL] != null) { + ({ icon, name } = endpointIcons[iconURL]); } return ( diff --git a/client/src/components/Messages/ContentRender.tsx b/client/src/components/Messages/ContentRender.tsx index 4cdba914e8..1d33ae5b63 100644 --- a/client/src/components/Messages/ContentRender.tsx +++ b/client/src/components/Messages/ContentRender.tsx @@ -138,6 +138,7 @@ const ContentRender = memo( enterEdit={enterEdit} siblingIdx={siblingIdx} setSiblingIdx={setSiblingIdx} + attachments={msg.attachments} />
diff --git a/client/src/components/SidePanel/Agents/AgentAvatar.tsx b/client/src/components/SidePanel/Agents/AgentAvatar.tsx index cb58e38c92..983044308b 100644 --- a/client/src/components/SidePanel/Agents/AgentAvatar.tsx +++ b/client/src/components/SidePanel/Agents/AgentAvatar.tsx @@ -21,7 +21,7 @@ import { useLocalize } from '~/hooks'; import { formatBytes } from '~/utils'; function Avatar({ - agent_id, + agent_id = '', avatar, createMutation, }: { @@ -31,9 +31,9 @@ function Avatar({ }) { const queryClient = useQueryClient(); const [menuOpen, setMenuOpen] = useState(false); + const [previewUrl, setPreviewUrl] = useState(''); const [progress, setProgress] = useState(1); const [input, setInput] = useState(null); - const [previewUrl, setPreviewUrl] = useState(null); const lastSeenCreatedId = useRef(null); const { data: fileConfig = defaultFileConfig } = useGetFileConfig({ select: (data) => mergeFileConfig(data), @@ -54,7 +54,8 @@ function Avatar({ } setInput(null); - setPreviewUrl(data.avatar?.filepath as string | null); + const newUrl = data.avatar?.filepath ?? ''; + setPreviewUrl(newUrl); const res = queryClient.getQueryData([ QueryKeys.agents, @@ -65,16 +66,15 @@ function Avatar({ return; } - const agents = - res.data.map((agent) => { - if (agent.id === agent_id) { - return { - ...agent, - ...data, - }; - } - return agent; - }) ?? []; + const agents = res.data.map((agent) => { + if (agent.id === agent_id) { + return { + ...agent, + ...data, + }; + } + return agent; + }); queryClient.setQueryData([QueryKeys.agents, defaultOrderQuery], { ...res, @@ -86,7 +86,7 @@ function Avatar({ onError: (error) => { console.error('Error:', error); setInput(null); - setPreviewUrl(null); + setPreviewUrl(''); showToast({ message: localize('com_ui_upload_error'), status: 'error' }); setProgress(1); }, @@ -103,8 +103,10 @@ function Avatar({ }, [input]); useEffect(() => { - if (avatar) { - setPreviewUrl((avatar.filepath as string | undefined) ?? null); + if (avatar && avatar.filepath) { + setPreviewUrl(avatar.filepath); + } else { + setPreviewUrl(''); } }, [avatar]); @@ -147,29 +149,31 @@ function Avatar({ const handleFileChange = (event: React.ChangeEvent): void => { const file = event.target.files?.[0]; + const sizeLimit = fileConfig.avatarSizeLimit ?? 0; - if (fileConfig.avatarSizeLimit && file && file.size <= fileConfig.avatarSizeLimit) { + if (sizeLimit && file && file.size <= sizeLimit) { setInput(file); setMenuOpen(false); - if (!agent_id) { + const currentId = agent_id ?? ''; + if (!currentId) { return; } const formData = new FormData(); formData.append('file', file, file.name); - formData.append('agent_id', agent_id); + formData.append('agent_id', currentId); if (typeof avatar === 'object') { formData.append('avatar', JSON.stringify(avatar)); } uploadAvatar({ - agent_id, + agent_id: currentId, formData, }); } else { - const megabytes = fileConfig.avatarSizeLimit ? formatBytes(fileConfig.avatarSizeLimit) : 2; + const megabytes = sizeLimit ? formatBytes(sizeLimit) : 2; showToast({ message: localize('com_ui_upload_invalid_var', megabytes + ''), status: 'error', diff --git a/client/src/components/SidePanel/Agents/AgentConfig.tsx b/client/src/components/SidePanel/Agents/AgentConfig.tsx index a35aa8e2ea..aca975d9b4 100644 --- a/client/src/components/SidePanel/Agents/AgentConfig.tsx +++ b/client/src/components/SidePanel/Agents/AgentConfig.tsx @@ -1,19 +1,22 @@ import React, { useState, useMemo, useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { Controller, useWatch, useFormContext } from 'react-hook-form'; -import { QueryKeys, Capabilities, EModelEndpoint } from 'librechat-data-provider'; +import { QueryKeys, AgentCapabilities, EModelEndpoint, SystemRoles } from 'librechat-data-provider'; import type { TConfig, TPlugin } from 'librechat-data-provider'; import type { AgentForm, AgentPanelProps } from '~/common'; import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils'; import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider'; +import { useToastContext, useFileMapContext } from '~/Providers'; import { icons } from '~/components/Chat/Menus/Endpoints/Icons'; import Action from '~/components/SidePanel/Builder/Action'; -import { useLocalize } from '~/hooks'; import { ToolSelectDialog } from '~/components/Tools'; -import { useToastContext } from '~/Providers'; +import { useLocalize, useAuthContext } from '~/hooks'; +import CapabilitiesForm from './CapabilitiesForm'; +import { processAgentOption } from '~/utils'; import { Spinner } from '~/components/svg'; import DeleteButton from './DeleteButton'; import AgentAvatar from './AgentAvatar'; +import FileSearch from './FileSearch'; import ShareAgent from './ShareAgent'; import AgentTool from './AgentTool'; import { Panel } from '~/common'; @@ -33,6 +36,8 @@ export default function AgentConfig({ setActivePanel, setCurrentAgentId, }: AgentPanelProps & { agentsConfig?: TConfig | null }) { + const { user } = useAuthContext(); + const fileMap = useFileMapContext(); const queryClient = useQueryClient(); const allTools = queryClient.getQueryData([QueryKeys.tools]) ?? []; @@ -51,21 +56,41 @@ export default function AgentConfig({ const agent_id = useWatch({ control, name: 'id' }); const toolsEnabled = useMemo( - () => agentsConfig?.capabilities?.includes(Capabilities.tools), + () => agentsConfig?.capabilities?.includes(AgentCapabilities.tools), [agentsConfig], ); const actionsEnabled = useMemo( - () => agentsConfig?.capabilities?.includes(Capabilities.actions), + () => agentsConfig?.capabilities?.includes(AgentCapabilities.actions), [agentsConfig], ); - // const retrievalEnabled = useMemo( - // () => agentsConfig?.capabilities?.includes(Capabilities.retrieval), - // [agentsConfig], - // ); - // const codeEnabled = useMemo( - // () => agentsConfig?.capabilities?.includes(Capabilities.code_interpreter), - // [agentsConfig], - // ); + const fileSearchEnabled = useMemo( + () => agentsConfig?.capabilities?.includes(AgentCapabilities.file_search) ?? false, + [agentsConfig], + ); + const codeEnabled = useMemo( + () => agentsConfig?.capabilities?.includes(AgentCapabilities.execute_code) ?? false, + [agentsConfig], + ); + + const knowledge_files = useMemo(() => { + if (typeof agent === 'string') { + return []; + } + + if (agent?.id !== agent_id) { + return []; + } + + if (agent.knowledge_files) { + return agent.knowledge_files; + } + + const _agent = processAgentOption({ + agent, + fileMap, + }); + return _agent.knowledge_files ?? []; + }, [agent, agent_id, fileMap]); /* Mutations */ const update = useUpdateAgentMutation({ @@ -118,8 +143,6 @@ export default function AgentConfig({ setActivePanel(Panel.actions); }, [agent_id, setActivePanel, showToast, localize]); - // Provider Icon logic - const providerValue = typeof provider === 'string' ? provider : provider?.value; let endpointType: EModelEndpoint | undefined; let endpointIconURL: string | undefined; @@ -280,10 +303,17 @@ export default function AgentConfig({
+ + {/* File Search */} + {fileSearchEnabled && } {/* Agent Tools & Actions */}
@@ -344,11 +374,14 @@ export default function AgentConfig({ setCurrentAgentId={setCurrentAgentId} createMutation={create} /> - + {(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) && ( + + )} {/* Submit Button */}
- {activePanel === Panel.model ? ( - - ) : null} - {activePanel === Panel.builder ? ( + {!canEditAgent && ( +
+
+

+ {localize('com_agents_not_available')} +

+

{localize('com_agents_no_access')}

+
+
+ )} + {canEditAgent && activePanel === Panel.model && ( + + )} + {canEditAgent && activePanel === Panel.builder && ( - ) : null} + )} ); diff --git a/client/src/components/SidePanel/Agents/AgentPanelSkeleton.tsx b/client/src/components/SidePanel/Agents/AgentPanelSkeleton.tsx new file mode 100644 index 0000000000..a5575c972f --- /dev/null +++ b/client/src/components/SidePanel/Agents/AgentPanelSkeleton.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { Skeleton } from '~/components/ui'; + +export default function AgentPanelSkeleton() { + return ( +
+ {/* Agent Select and Button */} +
+ + +
+ +
+ {/* Avatar */} +
+
+ +
+ {/* Name */} + + + +
+ + {/* Description */} +
+ + +
+ + {/* Instructions */} +
+ + +
+ + {/* Model and Provider */} +
+ + +
+ + {/* Capabilities */} +
+ + + +
+ + {/* Tools & Actions */} +
+ + + +
+ + +
+
+ + {/* Bottom Buttons */} +
+ + + +
+
+
+ ); +} diff --git a/client/src/components/SidePanel/Agents/AgentPanelSwitch.tsx b/client/src/components/SidePanel/Agents/AgentPanelSwitch.tsx index 3885774f01..0695a11462 100644 --- a/client/src/components/SidePanel/Agents/AgentPanelSwitch.tsx +++ b/client/src/components/SidePanel/Agents/AgentPanelSwitch.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useMemo } from 'react'; -import { Capabilities } from 'librechat-data-provider'; +import { EModelEndpoint } from 'librechat-data-provider'; import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import type { ActionsEndpoint } from '~/common'; import type { Action, TConfig, TEndpointsConfig } from 'librechat-data-provider'; @@ -18,19 +18,14 @@ export default function AgentPanelSwitch() { const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); const agentsConfig = useMemo( - () => - // endpointsConfig?.[EModelEndpoint.agents] ?? - ({ - // for testing purposes - capabilities: [Capabilities.tools, Capabilities.actions], - } as TConfig), - // [endpointsConfig]); - [], + () => endpointsConfig?.[EModelEndpoint.agents] ?? ({} as TConfig | null), + [endpointsConfig], ); useEffect(() => { - if (conversation?.agent_id) { - setCurrentAgentId(conversation?.agent_id); + const agent_id = conversation?.agent_id ?? ''; + if (agent_id) { + setCurrentAgentId(agent_id); } }, [conversation?.agent_id]); diff --git a/client/src/components/SidePanel/Agents/AgentSelect.tsx b/client/src/components/SidePanel/Agents/AgentSelect.tsx index 7803cca464..bd03b4a3a2 100644 --- a/client/src/components/SidePanel/Agents/AgentSelect.tsx +++ b/client/src/components/SidePanel/Agents/AgentSelect.tsx @@ -1,21 +1,21 @@ import { Plus, EarthIcon } from 'lucide-react'; import { useCallback, useEffect, useRef } from 'react'; import { useGetStartupConfig } from 'librechat-data-provider/react-query'; -import { Capabilities, defaultAgentFormValues } from 'librechat-data-provider'; +import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provider'; +import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query'; import type { Agent, AgentCreateParams } from 'librechat-data-provider'; -import type { UseMutationResult } from '@tanstack/react-query'; import type { UseFormReset } from 'react-hook-form'; -import type { AgentCapabilities, AgentForm, TAgentOption } from '~/common'; +import type { TAgentCapabilities, AgentForm, TAgentOption } from '~/common'; import { cn, createDropdownSetter, createProviderOption, processAgentOption } from '~/utils'; -import { useListAgentsQuery, useGetAgentByIdQuery } from '~/data-provider'; import SelectDropDown from '~/components/ui/SelectDropDown'; -// import { useFileMapContext } from '~/Providers'; +import { useListAgentsQuery } from '~/data-provider'; import { useLocalize } from '~/hooks'; const keys = new Set(Object.keys(defaultAgentFormValues)); export default function AgentSelect({ reset, + agentQuery, value: currentAgentValue, selectedAgentId = null, setCurrentAgentId, @@ -24,12 +24,11 @@ export default function AgentSelect({ reset: UseFormReset; value?: TAgentOption; selectedAgentId: string | null; + agentQuery: QueryObserverResult; setCurrentAgentId: React.Dispatch>; createMutation: UseMutationResult; }) { const localize = useLocalize(); - // TODO: file handling for agents - // const fileMap = useFileMapContext(); const lastSelectedAgent = useRef(null); const { data: startupConfig } = useGetStartupConfig(); @@ -39,15 +38,10 @@ export default function AgentSelect({ processAgentOption({ agent, instanceProjectId: startupConfig?.instanceProjectId, - /* fileMap */ }), ), }); - const agentQuery = useGetAgentByIdQuery(selectedAgentId ?? '', { - enabled: !!(selectedAgentId ?? ''), - }); - const resetAgentForm = useCallback( (fullAgent: Agent) => { const { instanceProjectId } = startupConfig ?? {}; @@ -61,17 +55,26 @@ export default function AgentSelect({ icon: isGlobal ? : null, }; - const actions: AgentCapabilities = { - [Capabilities.code_interpreter]: false, - [Capabilities.image_vision]: false, - [Capabilities.retrieval]: false, + const capabilities: TAgentCapabilities = { + [AgentCapabilities.execute_code]: false, + [AgentCapabilities.file_search]: false, }; - const formValues: Partial = { - ...actions, + const agentTools: string[] = []; + (fullAgent.tools ?? []).forEach((tool) => { + if (capabilities[tool] !== undefined) { + capabilities[tool] = true; + return; + } + + agentTools.push(tool); + }); + + const formValues: Partial = { + ...capabilities, agent: update, model: update.model, - tools: update.tools ?? [], + tools: agentTools, }; Object.entries(fullAgent).forEach(([name, value]) => { @@ -91,7 +94,7 @@ export default function AgentSelect({ reset(formValues); }, - [reset], + [reset, startupConfig], ); const onSelect = useCallback( diff --git a/client/src/components/SidePanel/Agents/CapabilitiesForm.tsx b/client/src/components/SidePanel/Agents/CapabilitiesForm.tsx index 408f4458ba..064ab26646 100644 --- a/client/src/components/SidePanel/Agents/CapabilitiesForm.tsx +++ b/client/src/components/SidePanel/Agents/CapabilitiesForm.tsx @@ -1,12 +1,12 @@ import { useMemo } from 'react'; // import { Capabilities } from 'librechat-data-provider'; -import { useFormContext, useWatch } from 'react-hook-form'; +// import { useFormContext, useWatch } from 'react-hook-form'; import type { TConfig } from 'librechat-data-provider'; -import type { AgentForm } from '~/common'; +// import type { AgentForm } from '~/common'; // import ImageVision from './ImageVision'; import { useLocalize } from '~/hooks'; import Retrieval from './Retrieval'; -import CodeFiles from './CodeFiles'; +// import CodeFiles from './CodeFiles'; import Code from './Code'; export default function CapabilitiesForm({ @@ -20,25 +20,21 @@ export default function CapabilitiesForm({ }) { const localize = useLocalize(); - const methods = useFormContext(); - const { control } = methods; - const agent = useWatch({ control, name: 'agent' }); - const agent_id = useWatch({ control, name: 'id' }); - const files = useMemo(() => { - if (typeof agent === 'string') { - return []; - } - return agent?.code_files; - }, [agent]); + // const methods = useFormContext(); + // const { control } = methods; + // const agent = useWatch({ control, name: 'agent' }); + // const agent_id = useWatch({ control, name: 'id' }); + // const files = useMemo(() => { + // if (typeof agent === 'string') { + // return []; + // } + // return agent?.code_files; + // }, [agent]); const retrievalModels = useMemo( () => new Set(agentsConfig?.retrievalModels ?? []), [agentsConfig], ); - // const imageVisionEnabled = useMemo( - // () => agentsConfig?.capabilities?.includes(Capabilities.image_vision), - // [agentsConfig], - // ); return (
@@ -50,10 +46,10 @@ export default function CapabilitiesForm({
- {codeEnabled && } - {retrievalEnabled && } + {codeEnabled === true && } + {retrievalEnabled === true && } {/* {imageVisionEnabled && version == 1 && } */} - {codeEnabled && } + {/* {codeEnabled && } */}
); diff --git a/client/src/components/SidePanel/Agents/Code.tsx b/client/src/components/SidePanel/Agents/Code.tsx index 27c3b42c21..bb3d383429 100644 --- a/client/src/components/SidePanel/Agents/Code.tsx +++ b/client/src/components/SidePanel/Agents/Code.tsx @@ -1,4 +1,4 @@ -import { Capabilities } from 'librechat-data-provider'; +import { AgentCapabilities } from 'librechat-data-provider'; import { useFormContext, Controller } from 'react-hook-form'; import type { AgentForm } from '~/common'; import { @@ -22,7 +22,7 @@ export default function Code() {
( )} /> -
+
+
-

+

{/* // TODO: add a Code Interpreter description */}

diff --git a/client/src/components/SidePanel/Agents/FileSearch.tsx b/client/src/components/SidePanel/Agents/FileSearch.tsx new file mode 100644 index 0000000000..84263f3566 --- /dev/null +++ b/client/src/components/SidePanel/Agents/FileSearch.tsx @@ -0,0 +1,120 @@ +import { useState, useRef, useEffect } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { + EModelEndpoint, + EToolResources, + mergeFileConfig, + AgentCapabilities, + retrievalMimeTypes, + fileConfig as defaultFileConfig, +} from 'librechat-data-provider'; +import type { ExtendedFile, AgentForm } from '~/common'; +import FileRow from '~/components/Chat/Input/Files/FileRow'; +import FileSearchCheckbox from './FileSearchCheckbox'; +import { useGetFileConfig } from '~/data-provider'; +import { AttachmentIcon } from '~/components/svg'; +import { useFileHandling } from '~/hooks/Files'; +import useLocalize from '~/hooks/useLocalize'; +import { useChatContext } from '~/Providers'; + +export default function FileSearch({ + agent_id, + files: _files, +}: { + agent_id: string; + files?: [string, ExtendedFile][]; +}) { + const localize = useLocalize(); + const { setFilesLoading } = useChatContext(); + const { watch } = useFormContext(); + const fileInputRef = useRef(null); + const [files, setFiles] = useState>(new Map()); + + const { data: fileConfig = defaultFileConfig } = useGetFileConfig({ + select: (data) => mergeFileConfig(data), + }); + + const { handleFileChange } = useFileHandling({ + overrideEndpoint: EModelEndpoint.agents, + additionalMetadata: { agent_id, tool_resource: EToolResources.file_search }, + fileSetter: setFiles, + }); + + useEffect(() => { + if (_files) { + setFiles(new Map(_files)); + } + }, [_files]); + + const fileSearchChecked = watch(AgentCapabilities.file_search); + + const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.agents]; + const disabled = endpointFileConfig.disabled ?? false; + + if (disabled === true) { + return null; + } + + const handleButtonClick = () => { + // necessary to reset the input + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + fileInputRef.current?.click(); + }; + + return ( +
+
+ + + +
+ +
+
+ +
+ {/* Disabled Message */} + {agent_id ? null : ( +
+ {localize('com_agents_file_search_disabled')} +
+ )} + {/* Knowledge Files */} + + retrievalMimeTypes.some((regex) => regex.test(file.type ?? '')) + } + Wrapper={({ children }) =>
{children}
} + /> +
+
+ ); +} diff --git a/client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx b/client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx new file mode 100644 index 0000000000..4d9dd85f5f --- /dev/null +++ b/client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx @@ -0,0 +1,70 @@ +import { AgentCapabilities } from 'librechat-data-provider'; +import { useFormContext, Controller } from 'react-hook-form'; +import type { AgentForm } from '~/common'; +import { + Checkbox, + HoverCard, + HoverCardContent, + HoverCardPortal, + HoverCardTrigger, +} from '~/components/ui'; +import { CircleHelpIcon } from '~/components/svg'; +import { useLocalize } from '~/hooks'; +import { ESide } from '~/common'; + +export default function FileSearchCheckbox() { + const localize = useLocalize(); + const methods = useFormContext(); + const { control, setValue, getValues } = methods; + + return ( + <> + +
+ ( + + )} + /> + + + +
+

+ {localize('com_agents_file_search_info')} +

+
+
+
+
+
+ + ); +} diff --git a/client/src/components/SidePanel/Agents/ModelPanel.tsx b/client/src/components/SidePanel/Agents/ModelPanel.tsx index 28f5fb91d1..0f61fe74a6 100644 --- a/client/src/components/SidePanel/Agents/ModelPanel.tsx +++ b/client/src/components/SidePanel/Agents/ModelPanel.tsx @@ -1,13 +1,18 @@ -import { useEffect, useMemo } from 'react'; +import React, { useMemo, useEffect } from 'react'; import { ChevronLeft } from 'lucide-react'; -import { Controller, useFormContext } from 'react-hook-form'; -import type { AgentForm, AgentModelPanelProps } from '~/common'; -import { SelectDropDown, ModelParameters } from '~/components/ui'; -import { cn, cardStyle } from '~/utils'; +import { getSettingsKeys } from 'librechat-data-provider'; +import { useFormContext, Controller } from 'react-hook-form'; +import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; +import type * as t from 'librechat-data-provider'; +import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common'; +import { componentMapping } from '~/components/SidePanel/Parameters/components'; +import { agentSettings } from '~/components/SidePanel/Parameters/settings'; +import { getEndpointField, cn, cardStyle } from '~/utils'; +import { SelectDropDown } from '~/components/ui'; import { useLocalize } from '~/hooks'; import { Panel } from '~/common'; -export default function ModelPanel({ +export default function Parameters({ setActivePanel, providers, models: modelsData, @@ -15,30 +20,56 @@ export default function ModelPanel({ const localize = useLocalize(); const { control, setValue, watch } = useFormContext(); - const model = watch('model'); + const modelParameters = watch('model_parameters'); const providerOption = watch('provider'); + const model = watch('model'); const provider = useMemo(() => { - if (!providerOption) { - return ''; - } - - return typeof providerOption === 'string' ? providerOption : providerOption.value; + const value = + typeof providerOption === 'string' + ? providerOption + : (providerOption as StringOption | undefined)?.value; + return value ?? ''; }, [providerOption]); const models = useMemo(() => (provider ? modelsData[provider] : []), [modelsData, provider]); useEffect(() => { - if (provider && model) { - const modelExists = models.includes(model); + const _model = model ?? ''; + if (provider && _model) { + const modelExists = models.includes(_model); if (!modelExists) { const newModels = modelsData[provider]; setValue('model', newModels[0] ?? ''); } } + + if (provider && !_model) { + setValue('model', models[0] ?? ''); + } }, [provider, models, modelsData, setValue, model]); + const { data: endpointsConfig } = useGetEndpointsQuery(); + + const bedrockRegions = useMemo(() => { + return endpointsConfig?.[provider]?.availableRegions ?? []; + }, [endpointsConfig, provider]); + + const endpointType = useMemo( + () => getEndpointField(endpointsConfig, provider, 'type'), + [provider, endpointsConfig], + ); + + const parameters = useMemo(() => { + const [combinedKey, endpointKey] = getSettingsKeys(endpointType ?? provider, model ?? ''); + return agentSettings[combinedKey] ?? agentSettings[endpointKey]; + }, [endpointType, model, provider]); + + const setOption = (optionKey: keyof t.AgentModelParameters) => (value: t.AgentParameterValue) => { + setValue(`model_parameters.${optionKey}`, value); + }; + return ( -
+
- {/* Endpoint aka Provider for Agents */} -
- - ( - <> - + {/* Endpoint aka Provider for Agents */} +
+ + ( + <> + + {error && ( + + {localize('com_ui_field_required')} + )} - containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')} - /> - {error && ( - - {localize('com_ui_field_required')} - - )} - - )} - /> -
- {/* Model */} -
- - ( - <> - + )} + /> +
+ {/* Model */} +
+ + ( + <> + + {provider && error && ( + + {localize('com_ui_field_required')} + )} - containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')} - /> - {provider && error && ( - - {localize('com_ui_field_required')} - - )} - - )} - /> -
-
- ( - <> - - - )} - /> -
-
- ( - <> - - - )} - /> -
-
- ( - <> - - - )} - /> -
-
- ( - <> - - - )} - /> -
-
- ( - <> - - - )} - /> -
-
- ( - <> - - - )} - /> + + )} + /> +
+ {/* Model Parameters */} + {parameters && ( +
+
+ {' '} + {/* This is the parent element containing all settings */} + {/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */} + {parameters.map((setting) => { + const Component = componentMapping[setting.component]; + if (!Component) { + return null; + } + const { key, default: defaultValue, ...rest } = setting; + + if (key === 'region' && bedrockRegions.length) { + rest.options = bedrockRegions; + } + + return ( + } + /> + ); + })} +
+
+ )}
); } diff --git a/client/src/components/SidePanel/Agents/ShareAgent.tsx b/client/src/components/SidePanel/Agents/ShareAgent.tsx index 287e54b56e..f4182900d8 100644 --- a/client/src/components/SidePanel/Agents/ShareAgent.tsx +++ b/client/src/components/SidePanel/Agents/ShareAgent.tsx @@ -19,16 +19,19 @@ import { useLocalize } from '~/hooks'; type FormValues = { [Permissions.SHARED_GLOBAL]: boolean; + [Permissions.UPDATE]: boolean; }; export default function ShareAgent({ agent_id = '', agentName, projectIds = [], + isCollaborative = false, }: { agent_id?: string; agentName?: string; projectIds?: string[]; + isCollaborative?: boolean; }) { const localize = useLocalize(); const { showToast } = useToastContext(); @@ -40,6 +43,7 @@ export default function ShareAgent({ ); const { + watch, control, setValue, getValues, @@ -49,12 +53,22 @@ export default function ShareAgent({ mode: 'onChange', defaultValues: { [Permissions.SHARED_GLOBAL]: agentIsGlobal, + [Permissions.UPDATE]: isCollaborative, }, }); + const sharedGlobalValue = watch(Permissions.SHARED_GLOBAL); + + useEffect(() => { + if (!sharedGlobalValue) { + setValue(Permissions.UPDATE, false); + } + }, [sharedGlobalValue, setValue]); + useEffect(() => { setValue(Permissions.SHARED_GLOBAL, agentIsGlobal); - }, [agentIsGlobal, setValue]); + setValue(Permissions.UPDATE, isCollaborative); + }, [agentIsGlobal, isCollaborative, setValue]); const updateAgent = useUpdateAgentMutation({ onSuccess: (data) => { @@ -87,16 +101,30 @@ export default function ShareAgent({ const payload = {} as AgentUpdateParams; - if (data[Permissions.SHARED_GLOBAL]) { - payload.projectIds = [startupConfig.instanceProjectId]; - } else { - payload.removeProjectIds = [startupConfig.instanceProjectId]; + if (data[Permissions.UPDATE] !== isCollaborative) { + payload.isCollaborative = data[Permissions.UPDATE]; } - updateAgent.mutate({ - agent_id, - data: payload, - }); + if (data[Permissions.SHARED_GLOBAL] !== agentIsGlobal) { + if (data[Permissions.SHARED_GLOBAL]) { + payload.projectIds = [startupConfig.instanceProjectId]; + } else { + payload.removeProjectIds = [startupConfig.instanceProjectId]; + payload.isCollaborative = false; + } + } + + if (Object.keys(payload).length > 0) { + updateAgent.mutate({ + agent_id, + data: payload, + }); + } else { + showToast({ + message: localize('com_ui_no_changes'), + status: 'info', + }); + } }; return ( @@ -113,12 +141,12 @@ export default function ShareAgent({ )} type="button" > -
+
- + {localize( 'com_ui_share_var', @@ -133,11 +161,12 @@ export default function ShareAgent({ handleSubmit(onSubmit)(e); }} > -
+
+
+
+ + {/* */} +
+ ( diff --git a/client/src/components/SidePanel/Nav.tsx b/client/src/components/SidePanel/Nav.tsx index d2692c3e53..d5b35b5faf 100644 --- a/client/src/components/SidePanel/Nav.tsx +++ b/client/src/components/SidePanel/Nav.tsx @@ -2,10 +2,9 @@ import { useState } from 'react'; import * as AccordionPrimitive from '@radix-ui/react-accordion'; import type { NavLink, NavProps } from '~/common'; import { Accordion, AccordionItem, AccordionContent } from '~/components/ui/Accordion'; -import { buttonVariants } from '~/components/ui/Button'; import { TooltipAnchor, Button } from '~/components'; -import { cn, removeFocusOutlines } from '~/utils'; import { useLocalize } from '~/hooks'; +import { cn } from '~/utils'; export default function Nav({ links, isCollapsed, resize, defaultActive }: NavProps) { const localize = useLocalize(); @@ -20,7 +19,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr return (
@@ -76,12 +75,11 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr > {localize(link.title)} - {link.label && ( + {link.label != null && link.label && ( {link.label} diff --git a/client/src/components/SidePanel/Parameters/settings.ts b/client/src/components/SidePanel/Parameters/settings.ts index f9b6398ae2..d26f4b82ca 100644 --- a/client/src/components/SidePanel/Parameters/settings.ts +++ b/client/src/components/SidePanel/Parameters/settings.ts @@ -572,3 +572,12 @@ export const presetSettings: Record< [`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneralColumns, [`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneralColumns, }; + +export const agentSettings: Record = Object.entries( + presetSettings, +).reduce((acc, [key, value]) => { + if (value) { + acc[key] = value.col2; + } + return acc; +}, {}); diff --git a/client/src/components/SidePanel/Switcher.tsx b/client/src/components/SidePanel/Switcher.tsx index cf08de19e2..b3d23108a3 100644 --- a/client/src/components/SidePanel/Switcher.tsx +++ b/client/src/components/SidePanel/Switcher.tsx @@ -1,33 +1,17 @@ import { isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider'; import type { SwitcherProps } from '~/common'; -import { Separator } from '~/components/ui/Separator'; import AssistantSwitcher from './AssistantSwitcher'; import AgentSwitcher from './AgentSwitcher'; import ModelSwitcher from './ModelSwitcher'; export default function Switcher(props: SwitcherProps) { if (isAssistantsEndpoint(props.endpoint) && props.endpointKeyProvided) { - return ( - <> - - - - ); + return ; } else if (isAgentsEndpoint(props.endpoint) && props.endpointKeyProvided) { - return ( - <> - - - - ); + return ; } else if (isAssistantsEndpoint(props.endpoint)) { return null; } - return ( - <> - - - - ); + return ; } diff --git a/client/src/components/Tools/ToolSelectDialog.tsx b/client/src/components/Tools/ToolSelectDialog.tsx index 318606007b..895f8aff47 100644 --- a/client/src/components/Tools/ToolSelectDialog.tsx +++ b/client/src/components/Tools/ToolSelectDialog.tsx @@ -2,12 +2,13 @@ import { useEffect } from 'react'; import { Search, X } from 'lucide-react'; import { Dialog, DialogPanel, DialogTitle, Description } from '@headlessui/react'; import { useFormContext } from 'react-hook-form'; +import { isAgentsEndpoint } from 'librechat-data-provider'; import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query'; import type { AssistantsEndpoint, EModelEndpoint, - TError, TPluginAction, + TError, } from 'librechat-data-provider'; import type { TPluginStoreDialogProps } from '~/common/types'; import { PluginPagination, PluginAuthForm } from '~/components/Plugins/Store'; @@ -26,7 +27,8 @@ function ToolSelectDialog({ }) { const localize = useLocalize(); const { getValues, setValue } = useFormContext(); - const { data: tools = [] } = useAvailableToolsQuery(endpoint); + const { data: tools } = useAvailableToolsQuery(endpoint); + const isAgentTools = isAgentsEndpoint(endpoint); const { maxPage, @@ -54,8 +56,9 @@ function ToolSelectDialog({ const updateUserPlugins = useUpdateUserPluginsMutation(); const handleInstallError = (error: TError) => { setError(true); - if (error.response?.data?.message) { - setErrorMessage(error.response?.data?.message); + const errorMessage = error.response?.data?.message ?? ''; + if (errorMessage) { + setErrorMessage(errorMessage); } setTimeout(() => { setError(false); @@ -105,7 +108,7 @@ function ToolSelectDialog({ const getAvailablePluginFromKey = tools?.find((p) => p.pluginKey === pluginKey); setSelectedPlugin(getAvailablePluginFromKey); - const { authConfig, authenticated } = getAvailablePluginFromKey ?? {}; + const { authConfig, authenticated = false } = getAvailablePluginFromKey ?? {}; if (authConfig && authConfig.length > 0 && !authenticated) { setShowPluginAuthForm(true); @@ -159,7 +162,9 @@ function ToolSelectDialog({
- {localize('com_nav_tool_dialog')} + {isAgentTools + ? localize('com_nav_tool_dialog_agents') + : localize('com_nav_tool_dialog')} {localize('com_nav_tool_dialog_description')} diff --git a/client/src/components/svg/Sparkles.tsx b/client/src/components/svg/Sparkles.tsx index 39e6063dc1..14de690565 100644 --- a/client/src/components/svg/Sparkles.tsx +++ b/client/src/components/svg/Sparkles.tsx @@ -1,10 +1,10 @@ import { cn } from '~/utils'; -export default function Sparkles({ className = '' }) { +export default function Sparkles({ className = '', size = 24 }) { return ( ( + config?: UseQueryOptions, +): QueryObserverResult => { + return useQuery([QueryKeys.files], () => dataService.getFiles(), { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + ...config, + }); +}; + +export const useGetFileConfig = ( + config?: UseQueryOptions, +): QueryObserverResult => { + return useQuery( + [QueryKeys.fileConfig], + () => dataService.getFileConfig(), + { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + ...config, + }, + ); +}; + +export const useFileDownload = (userId?: string, file_id?: string): QueryObserverResult => { + const queryClient = useQueryClient(); + return useQuery( + [QueryKeys.fileDownload, file_id], + async () => { + if (!userId || !file_id) { + console.warn('No user ID provided for file download'); + return; + } + const response = await dataService.getFileDownload(userId, file_id); + const blob = response.data; + const downloadURL = window.URL.createObjectURL(blob); + try { + const metadata: t.TFile | undefined = JSON.parse(response.headers['x-file-metadata']); + if (!metadata) { + console.warn('No metadata found for file download', response.headers); + return downloadURL; + } + + addFileToCache(queryClient, metadata); + } catch (e) { + console.error('Error parsing file metadata, skipped updating file query cache', e); + } + + return downloadURL; + }, + { + enabled: false, + retry: false, + }, + ); +}; + +export const useCodeOutputDownload = (url = ''): QueryObserverResult => { + return useQuery( + [QueryKeys.fileDownload, url], + async () => { + if (!url) { + console.warn('No user ID provided for file download'); + return; + } + const response = await dataService.getCodeOutputDownload(url); + const blob = response.data; + const downloadURL = window.URL.createObjectURL(blob); + return downloadURL; + }, + { + enabled: false, + retry: false, + }, + ); +}; diff --git a/client/src/data-provider/index.ts b/client/src/data-provider/index.ts index 2cfe6520db..1729c969bb 100644 --- a/client/src/data-provider/index.ts +++ b/client/src/data-provider/index.ts @@ -1,3 +1,4 @@ +export * from './Files'; export * from './connection'; export * from './mutations'; export * from './prompts'; diff --git a/client/src/data-provider/mutations.ts b/client/src/data-provider/mutations.ts index 339d269476..b92a5920d0 100644 --- a/client/src/data-provider/mutations.ts +++ b/client/src/data-provider/mutations.ts @@ -9,7 +9,7 @@ import { import { useSetRecoilState } from 'recoil'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { dataService, MutationKeys, QueryKeys, defaultOrderQuery } from 'librechat-data-provider'; -import type t from 'librechat-data-provider'; +import type * as t from 'librechat-data-provider'; import type { InfiniteData, UseMutationResult } from '@tanstack/react-query'; import useUpdateTagsInConvo from '~/hooks/Conversations/useUpdateTagsInConvo'; import { updateConversationTag } from '~/utils/conversationTags'; @@ -537,32 +537,33 @@ export const useDeleteConversationMutation = ( (payload: t.TDeleteConversationRequest) => dataService.deleteConversation(payload), { onSuccess: (_data, vars, context) => { - if (!vars.conversationId) { + const conversationId = vars.conversationId ?? ''; + if (!conversationId) { return; } - const handleDelete = (convoData) => { + const handleDelete = (convoData: t.ConversationData | undefined) => { if (!convoData) { return convoData; } return normalizeData( - deleteConversation(convoData, vars.conversationId as string), + deleteConversation(convoData, conversationId), 'conversations', - convoData.pages[0].pageSize, + Number(convoData.pages[0].pageSize), ); }; - queryClient.setQueryData([QueryKeys.conversation, vars.conversationId], null); + queryClient.setQueryData([QueryKeys.conversation, conversationId], null); queryClient.setQueryData([QueryKeys.allConversations], handleDelete); queryClient.setQueryData( [QueryKeys.archivedConversations], handleDelete, ); const current = queryClient.getQueryData([QueryKeys.allConversations]); - refetch({ refetchPage: (page, index) => index === (current?.pages.length || 1) - 1 }); + refetch({ refetchPage: (page, index) => index === (current?.pages.length ?? 1) - 1 }); onSuccess?.(_data, vars, context); }, - ...(_options || {}), + ..._options, }, ); }; @@ -593,7 +594,7 @@ export const useForkConvoMutation = ( ); onSuccess?.(data, vars, context); }, - ...(_options || {}), + ..._options, }); }; @@ -642,7 +643,7 @@ export const useUploadFileMutation = ( return dataService.uploadFile(body); }, - ...(options || {}), + ...options, onSuccess: (data, formData, context) => { queryClient.setQueryData([QueryKeys.files], (_files) => [ data, @@ -650,11 +651,44 @@ export const useUploadFileMutation = ( ]); const endpoint = formData.get('endpoint'); - const assistant_id = formData.get('assistant_id'); const message_file = formData.get('message_file'); - const tool_resource = formData.get('tool_resource'); + const agent_id = (formData.get('agent_id') as string | undefined) ?? ''; + const assistant_id = (formData.get('assistant_id') as string | undefined) ?? ''; + const tool_resource = (formData.get('tool_resource') as string | undefined) ?? ''; - if (!assistant_id || message_file === 'true') { + if (message_file === 'true') { + onSuccess?.(data, formData, context); + return; + } + + if (agent_id && tool_resource) { + queryClient.setQueryData([QueryKeys.agent, agent_id], (agent) => { + if (!agent) { + return agent; + } + + const update = {}; + const prevResources = agent.tool_resources ?? {}; + const prevResource: t.ExecuteCodeResource | t.AgentFileSearchResource = agent + .tool_resources?.[tool_resource] ?? { + file_ids: [], + }; + if (!prevResource.file_ids) { + prevResource.file_ids = []; + } + prevResource.file_ids.push(data.file_id); + update['tool_resources'] = { + ...prevResources, + [tool_resource]: prevResource, + }; + return { + ...agent, + ...update, + }; + }); + } + + if (!assistant_id) { onSuccess?.(data, formData, context); return; } @@ -679,13 +713,16 @@ export const useUploadFileMutation = ( } if (tool_resource === EToolResources.code_interpreter) { const prevResources = assistant.tool_resources ?? {}; - const prevResource = assistant.tool_resources?.[tool_resource as string] ?? { + const prevResource = assistant.tool_resources?.[tool_resource] ?? { file_ids: [], }; + if (!prevResource.file_ids) { + prevResource.file_ids = []; + } prevResource.file_ids.push(data.file_id); update['tool_resources'] = { ...prevResources, - [tool_resource as string]: prevResource, + [tool_resource]: prevResource, }; } return { @@ -712,9 +749,8 @@ export const useDeleteFilesMutation = ( const queryClient = useQueryClient(); const { onSuccess, ...options } = _options || {}; return useMutation([MutationKeys.fileDelete], { - mutationFn: (body: t.DeleteFilesBody) => - dataService.deleteFiles(body.files, body.assistant_id, body.tool_resource), - ...(options || {}), + mutationFn: (body: t.DeleteFilesBody) => dataService.deleteFiles(body), + ...options, onSuccess: (data, ...args) => { queryClient.setQueryData([QueryKeys.files], (cachefiles) => { const { files: filesDeleted } = args[0]; @@ -1228,6 +1264,8 @@ export const useUpdateAgentMutation = ( return agent; }), }); + + queryClient.setQueryData([QueryKeys.agent, variables.agent_id], updatedAgent); return options?.onSuccess?.(updatedAgent, variables, context); }, }, diff --git a/client/src/data-provider/queries.ts b/client/src/data-provider/queries.ts index 84e8ab2111..79b34dd74e 100644 --- a/client/src/data-provider/queries.ts +++ b/client/src/data-provider/queries.ts @@ -10,15 +10,12 @@ import type { UseInfiniteQueryOptions, QueryObserverResult, UseQueryOptions, - UseQueryResult, } from '@tanstack/react-query'; import type t from 'librechat-data-provider'; import type { Action, TPreset, - TFile, TPlugin, - FileConfig, ConversationListResponse, ConversationListParams, Assistant, @@ -32,36 +29,8 @@ import type { TCheckUserKeyResponse, SharedLinkListParams, SharedLinksResponse, - TUserTermsResponse, - TAcceptTermsResponse, } from 'librechat-data-provider'; -import { findPageForConversation, addFileToCache } from '~/utils'; - -export const useGetFiles = ( - config?: UseQueryOptions, -): QueryObserverResult => { - return useQuery([QueryKeys.files], () => dataService.getFiles(), { - refetchOnWindowFocus: false, - refetchOnReconnect: false, - refetchOnMount: false, - ...config, - }); -}; - -export const useGetFileConfig = ( - config?: UseQueryOptions, -): QueryObserverResult => { - return useQuery( - [QueryKeys.fileConfig], - () => dataService.getFileConfig(), - { - refetchOnWindowFocus: false, - refetchOnReconnect: false, - refetchOnMount: false, - ...config, - }, - ); -}; +import { findPageForConversation } from '~/utils'; export const useGetPresetsQuery = ( config?: UseQueryOptions, @@ -321,7 +290,7 @@ export const useGetAssistantByIdQuery = ( const queryClient = useQueryClient(); const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); const keyExpiry = queryClient.getQueryData([QueryKeys.name, endpoint]); - const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide; + const userProvidesKey = endpointsConfig?.[endpoint]?.userProvide ?? false; const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true; const enabled = !!endpointsConfig?.[endpoint] && keyProvided; const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint]; @@ -401,39 +370,6 @@ export const useGetAssistantDocsQuery = ( ); }; -export const useFileDownload = (userId?: string, file_id?: string): QueryObserverResult => { - const queryClient = useQueryClient(); - return useQuery( - [QueryKeys.fileDownload, file_id], - async () => { - if (!userId || !file_id) { - console.warn('No user ID provided for file download'); - return; - } - const response = await dataService.getFileDownload(userId, file_id); - const blob = response.data; - const downloadURL = window.URL.createObjectURL(blob); - try { - const metadata: TFile | undefined = JSON.parse(response.headers['x-file-metadata']); - if (!metadata) { - console.warn('No metadata found for file download', response.headers); - return downloadURL; - } - - addFileToCache(queryClient, metadata); - } catch (e) { - console.error('Error parsing file metadata, skipped updating file query cache', e); - } - - return downloadURL; - }, - { - enabled: false, - retry: false, - }, - ); -}; - /** * AGENTS */ diff --git a/client/src/hooks/Agents/useSelectAgent.ts b/client/src/hooks/Agents/useSelectAgent.ts index a5f1a9a6d1..1c785c0d98 100644 --- a/client/src/hooks/Agents/useSelectAgent.ts +++ b/client/src/hooks/Agents/useSelectAgent.ts @@ -17,7 +17,7 @@ export default function useSelectAgent() { ); const agentQuery = useGetAgentByIdQuery(selectedAgentId ?? '', { - enabled: !!selectedAgentId, + enabled: !!(selectedAgentId ?? ''), }); const updateConversation = useCallback( diff --git a/client/src/hooks/Files/useFileDeletion.ts b/client/src/hooks/Files/useFileDeletion.ts index 13eb5dd4c4..f457aba678 100644 --- a/client/src/hooks/Files/useFileDeletion.ts +++ b/client/src/hooks/Files/useFileDeletion.ts @@ -1,12 +1,7 @@ import debounce from 'lodash/debounce'; -import { FileSources, EToolResources } from 'librechat-data-provider'; +import { FileSources, EToolResources, removeNullishValues } from 'librechat-data-provider'; import { useCallback, useState, useEffect } from 'react'; -import type { - BatchFile, - TFile, - DeleteFilesResponse, - DeleteFilesBody, -} from 'librechat-data-provider'; +import type * as t from 'librechat-data-provider'; import type { UseMutateAsyncFunction } from '@tanstack/react-query'; import type { ExtendedFile, GenericSetter } from '~/common'; import useSetFilesToDelete from './useSetFilesToDelete'; @@ -15,21 +10,38 @@ type FileMapSetter = GenericSetter>; const useFileDeletion = ({ mutateAsync, + agent_id, assistant_id, tool_resource, }: { - mutateAsync: UseMutateAsyncFunction; + mutateAsync: UseMutateAsyncFunction; + agent_id?: string; assistant_id?: string; tool_resource?: EToolResources; }) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_batch, setFileDeleteBatch] = useState([]); + const [_batch, setFileDeleteBatch] = useState([]); const setFilesToDelete = useSetFilesToDelete(); const executeBatchDelete = useCallback( - (filesToDelete: BatchFile[], assistant_id?: string, tool_resource?: EToolResources) => { - console.log('Deleting files:', filesToDelete, assistant_id, tool_resource); - mutateAsync({ files: filesToDelete, assistant_id, tool_resource }); + ({ + filesToDelete, + agent_id, + assistant_id, + tool_resource, + }: { + filesToDelete: t.BatchFile[]; + agent_id?: string; + assistant_id?: string; + tool_resource?: EToolResources; + }) => { + const payload = removeNullishValues({ + agent_id, + assistant_id, + tool_resource, + }); + console.log('Deleting files:', filesToDelete, payload); + mutateAsync({ files: filesToDelete, ...payload }); setFileDeleteBatch([]); }, [mutateAsync], @@ -44,22 +56,22 @@ const useFileDeletion = ({ }, [debouncedDelete]); const deleteFile = useCallback( - ({ file: _file, setFiles }: { file: ExtendedFile | TFile; setFiles?: FileMapSetter }) => { + ({ file: _file, setFiles }: { file: ExtendedFile | t.TFile; setFiles?: FileMapSetter }) => { const { file_id, temp_file_id = '', filepath = '', source = FileSources.local, embedded, - attached, - } = _file as TFile & { attached?: boolean }; + attached = false, + } = _file as t.TFile & { attached?: boolean }; const progress = _file['progress'] ?? 1; if (progress < 1) { return; } - const file: BatchFile = { + const file: t.BatchFile = { file_id, embedded, filepath, @@ -83,15 +95,20 @@ const useFileDeletion = ({ setFileDeleteBatch((prevBatch) => { const newBatch = [...prevBatch, file]; - debouncedDelete(newBatch, assistant_id, tool_resource); + debouncedDelete({ + filesToDelete: newBatch, + agent_id, + assistant_id, + tool_resource, + }); return newBatch; }); }, - [debouncedDelete, setFilesToDelete, assistant_id, tool_resource], + [debouncedDelete, setFilesToDelete, agent_id, assistant_id, tool_resource], ); const deleteFiles = useCallback( - ({ files, setFiles }: { files: ExtendedFile[] | TFile[]; setFiles?: FileMapSetter }) => { + ({ files, setFiles }: { files: ExtendedFile[] | t.TFile[]; setFiles?: FileMapSetter }) => { const batchFiles = files.map((_file) => { const { file_id, embedded, filepath = '', source = FileSources.local } = _file; @@ -117,11 +134,15 @@ const useFileDeletion = ({ setFileDeleteBatch((prevBatch) => { const newBatch = [...prevBatch, ...batchFiles]; - debouncedDelete(newBatch, assistant_id); + debouncedDelete({ + filesToDelete: newBatch, + agent_id, + assistant_id, + }); return newBatch; }); }, - [debouncedDelete, setFilesToDelete, assistant_id], + [debouncedDelete, setFilesToDelete, agent_id, assistant_id], ); return { deleteFile, deleteFiles }; diff --git a/client/src/hooks/Files/useFileHandling.ts b/client/src/hooks/Files/useFileHandling.ts index b299373aab..ba7ea5dad1 100644 --- a/client/src/hooks/Files/useFileHandling.ts +++ b/client/src/hooks/Files/useFileHandling.ts @@ -40,6 +40,9 @@ const useFileHandling = (params?: UseFileHandling) => { params?.fileSetter ?? setFiles, ); + const agent_id = params?.additionalMetadata?.agent_id ?? ''; + const assistant_id = params?.additionalMetadata?.assistant_id ?? ''; + const { data: fileConfig = defaultFileConfig } = useGetFileConfig({ select: (data) => mergeFileConfig(data), }); @@ -84,13 +87,17 @@ const useFileHandling = (params?: UseFileHandling) => { onSuccess: (data) => { clearUploadTimer(data.temp_file_id); console.log('upload success', data); + if (agent_id) { + queryClient.refetchQueries([QueryKeys.agent, agent_id]); + return; + } updateFileById( data.temp_file_id, { progress: 0.9, filepath: data.filepath, }, - params?.additionalMetadata?.assistant_id ? true : false, + assistant_id ? true : false, ); setTimeout(() => { @@ -108,7 +115,7 @@ const useFileHandling = (params?: UseFileHandling) => { source: data.source, embedded: data.embedded, }, - params?.additionalMetadata?.assistant_id ? true : false, + assistant_id ? true : false, ); }, 300); }, @@ -118,51 +125,45 @@ const useFileHandling = (params?: UseFileHandling) => { clearUploadTimer(file_id as string); deleteFileById(file_id as string); setError( - (error as TError)?.response?.data?.message ?? 'An error occurred while uploading the file.', + (error as TError | undefined)?.response?.data?.message ?? + 'An error occurred while uploading the file.', ); }, }); const startUpload = async (extendedFile: ExtendedFile) => { - if (!endpoint) { - setError('An error occurred while uploading the file: Endpoint is undefined'); - return; - } - - startUploadTimer(extendedFile.file_id, extendedFile.file?.name || 'File', extendedFile.size); + const filename = extendedFile.file?.name ?? 'File'; + startUploadTimer(extendedFile.file_id, filename, extendedFile.size); const formData = new FormData(); - formData.append( - 'file', - extendedFile.file as File, - encodeURIComponent(extendedFile.file?.name || 'File'), - ); + formData.append('file', extendedFile.file as File, encodeURIComponent(filename)); formData.append('file_id', extendedFile.file_id); - if (extendedFile.width) { - formData.append('width', extendedFile.width?.toString()); + + const width = extendedFile.width ?? 0; + const height = extendedFile.height ?? 0; + if (width) { + formData.append('width', width.toString()); } - if (extendedFile.height) { - formData.append('height', extendedFile.height?.toString()); + if (height) { + formData.append('height', height.toString()); } if (params?.additionalMetadata) { - for (const [key, value] of Object.entries(params.additionalMetadata)) { + for (const [key, value = ''] of Object.entries(params.additionalMetadata)) { if (value) { formData.append(key, value); } } } - if ( - isAssistantsEndpoint(endpoint) && - !formData.get('assistant_id') && - conversation?.assistant_id - ) { + const convoAssistantId = conversation?.assistant_id ?? ''; + const convoModel = conversation?.model ?? ''; + if (isAssistantsEndpoint(endpoint) && !formData.get('assistant_id') && convoAssistantId) { const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint]; formData.append('version', version); - formData.append('assistant_id', conversation.assistant_id); - formData.append('model', conversation?.model ?? ''); + formData.append('assistant_id', convoAssistantId); + formData.append('model', convoModel); formData.append('message_file', 'true'); } if (isAssistantsEndpoint(endpoint) && !formData.get('version')) { @@ -238,7 +239,10 @@ const useFileHandling = (params?: UseFileHandling) => { (file) => `${file.file?.name ?? file.filename}-${file.size}-${file.type?.split('/')[0] ?? 'file'}`, ), - ...fileList.map((file) => `${file.name}-${file.size}-${file.type?.split('/')[0] ?? 'file'}`), + ...fileList.map( + (file: File | undefined) => + `${file?.name}-${file?.size}-${file?.type.split('/')[0] ?? 'file'}`, + ), ]; const uniqueFilesSet = new Set(combinedFilesInfo); @@ -300,7 +304,7 @@ const useFileHandling = (params?: UseFileHandling) => { addFile(extendedFile); - if (originalFile.type?.split('/')[0] === 'image') { + if (originalFile.type.split('/')[0] === 'image') { loadImage(extendedFile, preview); continue; } diff --git a/client/src/hooks/Files/useUpdateFiles.ts b/client/src/hooks/Files/useUpdateFiles.ts index d5e103877e..1598c7e7b2 100644 --- a/client/src/hooks/Files/useUpdateFiles.ts +++ b/client/src/hooks/Files/useUpdateFiles.ts @@ -20,11 +20,7 @@ export default function useUpdateFiles(setFiles: FileSetter) { }); }; - const updateFileById = ( - fileId: string, - updates: Partial, - isAssistantFile?: boolean, - ) => { + const updateFileById = (fileId: string, updates: Partial, isEntityFile = false) => { setFiles((currentFiles) => { if (!currentFiles.has(fileId)) { console.warn(`File with id ${fileId} not found.`); @@ -38,8 +34,8 @@ export default function useUpdateFiles(setFiles: FileSetter) { return currentFiles; } updatedFiles.set(fileId, { ...currentFile, ...updates }); - - if (updates['filepath'] && updates['progress'] !== 1 && !isAssistantFile) { + const filepath = updates['filepath'] ?? ''; + if (filepath && updates['progress'] !== 1 && !isEntityFile) { const files = Object.fromEntries(updatedFiles); setFilesToDelete(files); } diff --git a/client/src/hooks/SSE/index.ts b/client/src/hooks/SSE/index.ts index 9f18f6ffaf..fe0088747a 100644 --- a/client/src/hooks/SSE/index.ts +++ b/client/src/hooks/SSE/index.ts @@ -1,3 +1,4 @@ export { default as useSSE } from './useSSE'; export { default as useStepHandler } from './useStepHandler'; export { default as useContentHandler } from './useContentHandler'; +export { default as useAttachmentHandler } from './useAttachmentHandler'; diff --git a/client/src/hooks/SSE/useAttachmentHandler.ts b/client/src/hooks/SSE/useAttachmentHandler.ts new file mode 100644 index 0000000000..4cf0d84b76 --- /dev/null +++ b/client/src/hooks/SSE/useAttachmentHandler.ts @@ -0,0 +1,20 @@ +import { useSetRecoilState } from 'recoil'; +import type { TAttachment, EventSubmission } from 'librechat-data-provider'; +import store from '~/store'; + +export default function useAttachmentHandler() { + const setAttachmentsMap = useSetRecoilState(store.messageAttachmentsMap); + + return ({ data }: { data: TAttachment; submission: EventSubmission }) => { + const { messageId } = data; + + setAttachmentsMap((prevMap) => { + const messageAttachments = + (prevMap as Record)[messageId] || []; + return { + ...prevMap, + [messageId]: [...messageAttachments, data], + }; + }); + }; +} diff --git a/client/src/hooks/SSE/useEventHandlers.ts b/client/src/hooks/SSE/useEventHandlers.ts index 89f2e9af60..b3a57279b5 100644 --- a/client/src/hooks/SSE/useEventHandlers.ts +++ b/client/src/hooks/SSE/useEventHandlers.ts @@ -28,6 +28,7 @@ import { updateConversation, getConversationById, } from '~/utils'; +import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler'; import useContentHandler from '~/hooks/SSE/useContentHandler'; import useStepHandler from '~/hooks/SSE/useStepHandler'; import { useAuthContext } from '~/hooks/AuthContext'; @@ -62,7 +63,7 @@ export default function useEventHandlers({ setMessages, getMessages, setCompleted, - isAddedRequest, + isAddedRequest = false, setConversation, setIsSubmitting, newConversation, @@ -85,6 +86,7 @@ export default function useEventHandlers({ setIsSubmitting, lastAnnouncementTimeRef, }); + const attachmentHandler = useAttachmentHandler(); const messageHandler = useCallback( (data: string | undefined, submission: EventSubmission) => { @@ -138,15 +140,20 @@ export default function useEventHandlers({ const { requestMessage, responseMessage, conversation } = data; const { messages, isRegenerate = false } = submission; - const convoUpdate = conversation ?? submission.conversation; + const convoUpdate = + (conversation as TConversation | null) ?? (submission.conversation as TConversation); // update the messages if (isRegenerate) { - const messagesUpdate = [...messages, responseMessage].filter((msg) => msg); - setMessages(messagesUpdate); + const messagesUpdate = ( + [...messages, responseMessage] as Array + ).filter((msg) => msg); + setMessages(messagesUpdate as TMessage[]); } else { - const messagesUpdate = [...messages, requestMessage, responseMessage].filter((msg) => msg); - setMessages(messagesUpdate); + const messagesUpdate = ( + [...messages, requestMessage, responseMessage] as Array + ).filter((msg) => msg); + setMessages(messagesUpdate as TMessage[]); } const isNewConvo = conversation.conversationId !== submission.conversation.conversationId; @@ -208,7 +215,10 @@ export default function useEventHandlers({ setConversation((prevState) => { let title = prevState?.title; const parentId = requestMessage.parentMessageId; - if (parentId !== Constants.NO_PARENT && title?.toLowerCase()?.includes('new chat')) { + if ( + parentId !== Constants.NO_PARENT && + (title?.toLowerCase()?.includes('new chat') ?? false) + ) { const convos = queryClient.getQueryData([QueryKeys.allConversations]); const cachedConvo = getConversationById(convos, conversationId); title = cachedConvo?.title; @@ -289,7 +299,10 @@ export default function useEventHandlers({ setConversation((prevState) => { let title = prevState?.title; const parentId = isRegenerate ? userMessage.overrideParentMessageId : parentMessageId; - if (parentId !== Constants.NO_PARENT && title?.toLowerCase()?.includes('new chat')) { + if ( + parentId !== Constants.NO_PARENT && + (title?.toLowerCase()?.includes('new chat') ?? false) + ) { const convos = queryClient.getQueryData([QueryKeys.allConversations]); const cachedConvo = getConversationById(convos, conversationId); title = cachedConvo?.title; @@ -437,7 +450,7 @@ export default function useEventHandlers({ setCompleted((prev) => new Set(prev.add(initialResponse.messageId))); - const conversationId = userMessage.conversationId ?? submission.conversationId; + const conversationId = userMessage.conversationId ?? submission.conversationId ?? ''; const parseErrorResponse = (data: TResData | Partial) => { const metadata = data['responseMessage'] ?? data; @@ -456,7 +469,7 @@ export default function useEventHandlers({ }; if (!data) { - const convoId = conversationId ?? v4(); + const convoId = conversationId || v4(); const errorResponse = parseErrorResponse({ text: 'Error connecting to server, try refreshing the page.', ...submission, @@ -473,7 +486,8 @@ export default function useEventHandlers({ return; } - if (!conversationId && !data.conversationId) { + const receivedConvoId = data.conversationId ?? ''; + if (!conversationId && !receivedConvoId) { const convoId = v4(); const errorResponse = parseErrorResponse(data); setMessages([...messages, userMessage, errorResponse]); @@ -485,7 +499,7 @@ export default function useEventHandlers({ } setIsSubmitting(false); return; - } else if (!data.conversationId) { + } else if (!receivedConvoId) { const errorResponse = parseErrorResponse(data); setMessages([...messages, userMessage, errorResponse]); setIsSubmitting(false); @@ -500,9 +514,9 @@ export default function useEventHandlers({ }); setMessages([...messages, userMessage, errorResponse]); - if (data.conversationId && paramId === 'new' && newConversation) { + if (receivedConvoId && paramId === 'new' && newConversation) { newConversation({ - template: { conversationId: data.conversationId }, + template: { conversationId: receivedConvoId }, preset: tPresetSchema.parse(submission.conversation), }); } @@ -517,7 +531,8 @@ export default function useEventHandlers({ async (conversationId = '', submission: EventSubmission, messages?: TMessage[]) => { const runAbortKey = `${conversationId}:${messages?.[messages.length - 1]?.messageId ?? ''}`; console.log({ conversationId, submission, messages, runAbortKey }); - const { endpoint: _endpoint, endpointType } = submission.conversation || {}; + const { endpoint: _endpoint, endpointType } = + (submission.conversation as TConversation | null) ?? {}; const endpoint = endpointType ?? _endpoint; try { const response = await fetch(`${EndpointURLs[endpoint ?? '']}/abort`, { @@ -541,7 +556,7 @@ export default function useEventHandlers({ setIsSubmitting(false); return; } - if (data.final) { + if (data.final === true) { finalHandler(data, submission); } else { cancelHandler(data, submission); @@ -569,13 +584,13 @@ export default function useEventHandlers({ } catch (error) { console.error('Error cancelling request'); console.error(error); - const convoId = conversationId ?? v4(); + const convoId = conversationId || v4(); const text = submission.initialResponse.text.length > 45 ? submission.initialResponse.text : ''; const errorMessage = { ...submission, ...submission.initialResponse, - text: text ?? (error as Error).message ?? 'Error cancelling request', + text: (text || (error as Error | undefined)?.message) ?? 'Error cancelling request', unfinished: !!text.length, error: true, }; @@ -601,6 +616,7 @@ export default function useEventHandlers({ messageHandler, contentHandler, createdHandler, + attachmentHandler, abortConversation, }; } diff --git a/client/src/hooks/SSE/useSSE.ts b/client/src/hooks/SSE/useSSE.ts index 75a97e7b3a..1c3e51d101 100644 --- a/client/src/hooks/SSE/useSSE.ts +++ b/client/src/hooks/SSE/useSSE.ts @@ -10,7 +10,7 @@ import { isAssistantsEndpoint, } from 'librechat-data-provider'; import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query'; -import type { TMessage, TSubmission, EventSubmission } from 'librechat-data-provider'; +import type { TMessage, TSubmission, TPayload, EventSubmission } from 'librechat-data-provider'; import type { EventHandlerParams } from './useEventHandlers'; import type { TResData } from '~/common'; import { useGenTitleMutation } from '~/data-provider'; @@ -59,6 +59,7 @@ export default function useSSE( messageHandler, contentHandler, createdHandler, + attachmentHandler, abortConversation, } = useEventHandlers({ genTitle, @@ -88,7 +89,7 @@ export default function useSSE( const payloadData = createPayload(submission); let { payload } = payloadData; if (isAssistantsEndpoint(payload.endpoint) || isAgentsEndpoint(payload.endpoint)) { - payload = removeNullishValues(payload); + payload = removeNullishValues(payload) as TPayload; } let textIndex = null; @@ -98,6 +99,15 @@ export default function useSSE( headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, }); + events.onattachment = (e: MessageEvent) => { + try { + const data = JSON.parse(e.data); + attachmentHandler({ data, submission: submission as EventSubmission }); + } catch (error) { + console.error(error); + } + }; + events.onmessage = (e: MessageEvent) => { const data = JSON.parse(e.data); diff --git a/client/src/localization/languages/Ar.ts b/client/src/localization/languages/Ar.ts index 68336d393a..d61ef29316 100644 --- a/client/src/localization/languages/Ar.ts +++ b/client/src/localization/languages/Ar.ts @@ -376,7 +376,7 @@ export default { com_assistants_code_interpreter_files: 'الملفات التالية متاحة فقط لمفسر الشفرة:', com_assistants_retrieval: 'استرداد', com_assistants_search_name: 'البحث عن المساعدين بالاسم', - com_assistants_tools: 'أدوات المساعدين', + com_ui_tools: 'أدوات المساعدين', com_assistants_actions: 'إجراءات', com_assistants_add_tools: 'إضافة أدوات', com_assistants_add_actions: 'إضافة إجراءات', @@ -1964,7 +1964,7 @@ export const comparisons = { english: 'Search assistants by name', translated: 'البحث عن المساعدين بالاسم', }, - com_assistants_tools: { + com_ui_tools: { english: 'Tools', translated: 'أدوات المساعدين', }, diff --git a/client/src/localization/languages/Br.ts b/client/src/localization/languages/Br.ts index 8f4bd4a6b9..ce89f4d83d 100644 --- a/client/src/localization/languages/Br.ts +++ b/client/src/localization/languages/Br.ts @@ -6,21 +6,29 @@ export default { com_nav_convo_menu_options: 'Opções do Menu de Conversa', com_ui_artifacts: 'Artefatos', com_ui_artifacts_toggle: 'Alternar UI de Artefatos', - com_nav_info_code_artifacts: 'Habilita a exibição de artefatos de código experimental ao lado do chat', + com_nav_info_code_artifacts: + 'Habilita a exibição de artefatos de código experimental ao lado do chat', com_ui_include_shadcnui: 'Incluir instruções de componentes shadcn/ui', - com_nav_info_include_shadcnui: 'Quando habilitado, as instruções para usar componentes shadcn/ui serão incluídas. shadcn/ui é uma coleção de componentes reutilizáveis construídos usando Radix UI e Tailwind CSS. Nota: estas são instruções longas, você deve habilitar apenas se for importante informar o LLM sobre as importações e componentes corretos. Para mais informações sobre esses componentes, visite: https://ui.shadcn.com/', + com_nav_info_include_shadcnui: + 'Quando habilitado, as instruções para usar componentes shadcn/ui serão incluídas. shadcn/ui é uma coleção de componentes reutilizáveis construídos usando Radix UI e Tailwind CSS. Nota: estas são instruções longas, você deve habilitar apenas se for importante informar o LLM sobre as importações e componentes corretos. Para mais informações sobre esses componentes, visite: https://ui.shadcn.com/', com_ui_custom_prompt_mode: 'Modo de Prompt Personalizado', - com_nav_info_custom_prompt_mode: 'Quando habilitado, o prompt padrão do sistema de artefatos não será incluído. Todas as instruções de geração de artefatos devem ser fornecidas manualmente neste modo.', + com_nav_info_custom_prompt_mode: + 'Quando habilitado, o prompt padrão do sistema de artefatos não será incluído. Todas as instruções de geração de artefatos devem ser fornecidas manualmente neste modo.', com_ui_artifact_click: 'Clique para abrir', com_a11y_start: 'A IA começou a responder.', com_a11y_ai_composing: 'A IA ainda está compondo.', com_a11y_end: 'A IA terminou de responder.', - com_error_moderation: 'Parece que o conteúdo enviado foi sinalizado pelo nosso sistema de moderação por não estar alinhado com nossas diretrizes da comunidade. Não podemos prosseguir com este tópico específico. Se você tiver outras perguntas ou tópicos que gostaria de explorar, edite sua mensagem ou crie uma nova conversa.', - com_error_no_user_key: 'Nenhuma chave encontrada. Por favor, forneça uma chave e tente novamente.', + com_error_moderation: + 'Parece que o conteúdo enviado foi sinalizado pelo nosso sistema de moderação por não estar alinhado com nossas diretrizes da comunidade. Não podemos prosseguir com este tópico específico. Se você tiver outras perguntas ou tópicos que gostaria de explorar, edite sua mensagem ou crie uma nova conversa.', + com_error_no_user_key: + 'Nenhuma chave encontrada. Por favor, forneça uma chave e tente novamente.', com_error_no_base_url: 'Nenhuma URL base encontrada. Por favor, forneça uma e tente novamente.', - com_error_invalid_user_key: 'Chave fornecida inválida. Por favor, forneça uma chave válida e tente novamente.', - com_error_expired_user_key: 'A chave fornecida para {0} expirou em {1}. Por favor, forneça uma nova chave e tente novamente.', - com_error_input_length: 'A contagem de tokens da última mensagem é muito longa, excedendo o limite de tokens ({0} respectivamente). Por favor, encurte sua mensagem, ajuste o tamanho máximo do contexto nos parâmetros da conversa ou divida a conversa para continuar.', + com_error_invalid_user_key: + 'Chave fornecida inválida. Por favor, forneça uma chave válida e tente novamente.', + com_error_expired_user_key: + 'A chave fornecida para {0} expirou em {1}. Por favor, forneça uma nova chave e tente novamente.', + com_error_input_length: + 'A contagem de tokens da última mensagem é muito longa, excedendo o limite de tokens ({0} respectivamente). Por favor, encurte sua mensagem, ajuste o tamanho máximo do contexto nos parâmetros da conversa ou divida a conversa para continuar.', com_files_no_results: 'Nenhum resultado.', com_files_filter: 'Filtrar arquivos...', com_files_number_selected: '{0} de {1} arquivo(s) selecionado(s)', @@ -33,21 +41,27 @@ export default { com_sidepanel_conversation_tags: 'Marcadores', com_assistants_capabilities: 'Capacidades', com_assistants_file_search: 'Pesquisa de Arquivos', - com_assistants_file_search_info: 'A pesquisa de arquivos permite que o assistente tenha conhecimento dos arquivos que você ou seus usuários carregam. Uma vez que um arquivo é carregado, o assistente decide automaticamente quando recuperar o conteúdo com base nas solicitações do usuário. Anexar armazenamentos vetoriais para Pesquisa de Arquivos ainda não é suportado. Você pode anexá-los no Playground do Provedor ou anexar arquivos às mensagens para pesquisa de arquivos em uma base de thread.', - com_assistants_code_interpreter_info: 'O Interpretador de Código permite que o assistente escreva e execute código. Esta ferramenta pode processar arquivos com dados e formatações diversas, e gerar arquivos como gráficos.', + com_assistants_file_search_info: + 'A pesquisa de arquivos permite que o assistente tenha conhecimento dos arquivos que você ou seus usuários carregam. Uma vez que um arquivo é carregado, o assistente decide automaticamente quando recuperar o conteúdo com base nas solicitações do usuário. Anexar armazenamentos vetoriais para Pesquisa de Arquivos ainda não é suportado. Você pode anexá-los no Playground do Provedor ou anexar arquivos às mensagens para pesquisa de arquivos em uma base de thread.', + com_assistants_code_interpreter_info: + 'O Interpretador de Código permite que o assistente escreva e execute código. Esta ferramenta pode processar arquivos com dados e formatações diversas, e gerar arquivos como gráficos.', com_assistants_knowledge: 'Conhecimento', - com_assistants_knowledge_info: 'Se você carregar arquivos em Conhecimento, as conversas com seu Assistente podem incluir o conteúdo dos arquivos.', - com_assistants_knowledge_disabled: 'O assistente deve ser criado, e o Interpretador de Código ou Recuperação deve ser habilitado e salvo antes de carregar arquivos como Conhecimento.', + com_assistants_knowledge_info: + 'Se você carregar arquivos em Conhecimento, as conversas com seu Assistente podem incluir o conteúdo dos arquivos.', + com_assistants_knowledge_disabled: + 'O assistente deve ser criado, e o Interpretador de Código ou Recuperação deve ser habilitado e salvo antes de carregar arquivos como Conhecimento.', com_assistants_image_vision: 'Visão de Imagem', com_assistants_code_interpreter: 'Interpretador de Código', - com_assistants_code_interpreter_files: 'Os arquivos abaixo são apenas para o Interpretador de Código:', + com_assistants_code_interpreter_files: + 'Os arquivos abaixo são apenas para o Interpretador de Código:', com_assistants_retrieval: 'Recuperação', com_assistants_search_name: 'Pesquisar assistentes por nome', - com_assistants_tools: 'Ferramentas', + com_ui_tools: 'Ferramentas', com_assistants_actions: 'Ações', com_assistants_add_tools: 'Adicionar Ferramentas', com_assistants_add_actions: 'Adicionar Ações', - com_assistants_non_retrieval_model: 'A pesquisa de arquivos não está habilitada neste modelo. Por favor, selecione outro modelo.', + com_assistants_non_retrieval_model: + 'A pesquisa de arquivos não está habilitada neste modelo. Por favor, selecione outro modelo.', com_assistants_available_actions: 'Ações Disponíveis', com_assistants_running_action: 'Executando ação', com_assistants_completed_action: 'Conversou com {0}', @@ -58,7 +72,8 @@ export default { com_assistants_update_actions_success: 'Ação criada ou atualizada com sucesso', com_assistants_update_actions_error: 'Houve um erro ao criar ou atualizar a ação.', com_assistants_delete_actions_error: 'Houve um erro ao excluir a ação.', - com_assistants_actions_info: 'Permita que seu Assistente recupere informações ou execute ações via API\'s', + com_assistants_actions_info: + 'Permita que seu Assistente recupere informações ou execute ações via API\'s', com_assistants_name_placeholder: 'Opcional: O nome do assistente', com_assistants_instructions_placeholder: 'As instruções do sistema que o assistente usa', com_assistants_description_placeholder: 'Opcional: Descreva seu Assistente aqui', @@ -96,9 +111,11 @@ export default { com_ui_download_error: 'Erro ao baixar o arquivo. O arquivo pode ter sido excluído.', com_ui_attach_error_type: 'Tipo de arquivo não suportado para o endpoint:', com_ui_attach_error_openai: 'Não é possível anexar arquivos de Assistente a outros endpoints', - com_ui_attach_warn_endpoint: 'Arquivos não compatíveis podem ser ignorados sem uma ferramenta compatível', + com_ui_attach_warn_endpoint: + 'Arquivos não compatíveis podem ser ignorados sem uma ferramenta compatível', com_ui_attach_error_size: 'Limite de tamanho de arquivo excedido para o endpoint:', - com_ui_attach_error: 'Não é possível anexar o arquivo. Crie ou selecione uma conversa, ou tente atualizar a página.', + com_ui_attach_error: + 'Não é possível anexar o arquivo. Crie ou selecione uma conversa, ou tente atualizar a página.', com_ui_examples: 'Exemplos', com_ui_new_chat: 'Novo chat', com_ui_happy_birthday: 'É meu 1º aniversário!', @@ -162,11 +179,14 @@ export default { com_ui_search_categories: 'Pesquisar Categorias', com_ui_manage: 'Gerenciar', com_ui_variables: 'Variáveis', - com_ui_variables_info: 'Use chaves duplas no seu texto para criar variáveis, por exemplo, `{{exemplo de variável}}`, para preencher posteriormente ao usar o prompt.', + com_ui_variables_info: + 'Use chaves duplas no seu texto para criar variáveis, por exemplo, `{{exemplo de variável}}`, para preencher posteriormente ao usar o prompt.', com_ui_special_variables: 'Variáveis especiais:', - com_ui_special_variables_info: 'Use `{{current_date}}` para a data atual, e `{{current_user}}` para o nome da sua conta.', + com_ui_special_variables_info: + 'Use `{{current_date}}` para a data atual, e `{{current_user}}` para o nome da sua conta.', com_ui_dropdown_variables: 'Variáveis de dropdown:', - com_ui_dropdown_variables_info: 'Crie menus dropdown personalizados para seus prompts: `{{nome_da_variável:opção1|opção2|opção3}}`', + com_ui_dropdown_variables_info: + 'Crie menus dropdown personalizados para seus prompts: `{{nome_da_variável:opção1|opção2|opção3}}`', com_ui_showing: 'Mostrando', com_ui_of: 'de', com_ui_entries: 'Entradas', @@ -179,7 +199,8 @@ export default { com_ui_upload_success: 'Arquivo carregado com sucesso', com_ui_upload_error: 'Houve um erro ao carregar seu arquivo', com_ui_upload_invalid: 'Arquivo inválido para upload. Deve ser uma imagem não excedendo o limite', - com_ui_upload_invalid_var: 'Arquivo inválido para upload. Deve ser uma imagem não excedendo {0} MB', + com_ui_upload_invalid_var: + 'Arquivo inválido para upload. Deve ser uma imagem não excedendo {0} MB', com_ui_cancel: 'Cancelar', com_ui_save: 'Salvar', com_ui_renaming_var: 'Renomeando "{0}"', @@ -192,13 +213,20 @@ export default { com_ui_copied_to_clipboard: 'Copiado para a área de transferência', com_ui_fork: 'Bifurcar', com_ui_fork_info_1: 'Use esta configuração para bifurcar mensagens com o comportamento desejado.', - com_ui_fork_info_2: '"Bifurcação" refere-se à criação de uma nova conversa que começa/termina a partir de mensagens específicas na conversa atual, criando uma cópia de acordo com as opções selecionadas.', - com_ui_fork_info_3: 'A "mensagem alvo" refere-se à mensagem da qual este popup foi aberto, ou, se você marcar "{0}", a última mensagem na conversa.', - com_ui_fork_info_visible: 'Esta opção bifurca apenas as mensagens visíveis; em outras palavras, o caminho direto para a mensagem alvo, sem quaisquer ramificações.', - com_ui_fork_info_branches: 'Esta opção bifurca as mensagens visíveis, junto com ramificações relacionadas; em outras palavras, o caminho direto para a mensagem alvo, incluindo ramificações ao longo do caminho.', - com_ui_fork_info_target: 'Esta opção bifurca todas as mensagens até a mensagem alvo, incluindo seus vizinhos; em outras palavras, todos os ramos de mensagens, estejam ou não visíveis ou ao longo do mesmo caminho, estão incluídos.', - com_ui_fork_info_start: 'Se marcado, a bifurcação começará desta mensagem até a última mensagem na conversa, de acordo com o comportamento selecionado acima.', - com_ui_fork_info_remember: 'Marque isto para lembrar as opções que você seleciona para uso futuro, tornando mais rápido bifurcar conversas conforme preferido.', + com_ui_fork_info_2: + '"Bifurcação" refere-se à criação de uma nova conversa que começa/termina a partir de mensagens específicas na conversa atual, criando uma cópia de acordo com as opções selecionadas.', + com_ui_fork_info_3: + 'A "mensagem alvo" refere-se à mensagem da qual este popup foi aberto, ou, se você marcar "{0}", a última mensagem na conversa.', + com_ui_fork_info_visible: + 'Esta opção bifurca apenas as mensagens visíveis; em outras palavras, o caminho direto para a mensagem alvo, sem quaisquer ramificações.', + com_ui_fork_info_branches: + 'Esta opção bifurca as mensagens visíveis, junto com ramificações relacionadas; em outras palavras, o caminho direto para a mensagem alvo, incluindo ramificações ao longo do caminho.', + com_ui_fork_info_target: + 'Esta opção bifurca todas as mensagens até a mensagem alvo, incluindo seus vizinhos; em outras palavras, todos os ramos de mensagens, estejam ou não visíveis ou ao longo do mesmo caminho, estão incluídos.', + com_ui_fork_info_start: + 'Se marcado, a bifurcação começará desta mensagem até a última mensagem na conversa, de acordo com o comportamento selecionado acima.', + com_ui_fork_info_remember: + 'Marque isto para lembrar as opções que você seleciona para uso futuro, tornando mais rápido bifurcar conversas conforme preferido.', com_ui_fork_success: 'Conversa bifurcada com sucesso', com_ui_fork_processing: 'Bifurcando conversa...', com_ui_fork_error: 'Houve um erro ao bifurcar a conversa', @@ -207,12 +235,14 @@ export default { com_ui_fork_remember: 'Lembrar', com_ui_fork_split_target_setting: 'Iniciar bifurcação a partir da mensagem alvo por padrão', com_ui_fork_split_target: 'Iniciar bifurcação aqui', - com_ui_fork_remember_checked: 'Sua seleção será lembrada após o uso. Altere isso a qualquer momento nas configurações.', + com_ui_fork_remember_checked: + 'Sua seleção será lembrada após o uso. Altere isso a qualquer momento nas configurações.', com_ui_fork_all_target: 'Incluir todos para/de aqui', com_ui_fork_branches: 'Incluir ramificações relacionadas', com_ui_fork_visible: 'Apenas mensagens visíveis', com_ui_fork_from_message: 'Selecione uma opção de bifurcação', - com_ui_mention: 'Mencione um endpoint, assistente ou predefinição para alternar rapidamente para ele', + com_ui_mention: + 'Mencione um endpoint, assistente ou predefinição para alternar rapidamente para ele', com_ui_add_model_preset: 'Adicionar um modelo ou predefinição para uma resposta adicional', com_assistants_max_starters_reached: 'Número máximo de iniciadores de conversa atingido', com_ui_regenerate: 'Regenerar', @@ -280,10 +310,14 @@ export default { com_ui_share_error: 'Houve um erro ao compartilhar o link do chat', com_ui_share_retrieve_error: 'Houve um erro ao recuperar os links compartilhados', com_ui_share_delete_error: 'Houve um erro ao excluir o link compartilhado', - com_ui_share_create_message: 'Seu nome e quaisquer mensagens que você adicionar após o compartilhamento permanecerão privadas.', - com_ui_share_created_message: 'Um link compartilhado para o seu chat foi criado. Gerencie chats compartilhados anteriormente a qualquer momento via Configurações.', - com_ui_share_update_message: 'Seu nome, instruções personalizadas e quaisquer mensagens que você adicionar após o compartilhamento permanecerão privadas.', - com_ui_share_updated_message: 'Um link compartilhado para o seu chat foi atualizado. Gerencie chats compartilhados anteriormente a qualquer momento via Configurações.', + com_ui_share_create_message: + 'Seu nome e quaisquer mensagens que você adicionar após o compartilhamento permanecerão privadas.', + com_ui_share_created_message: + 'Um link compartilhado para o seu chat foi criado. Gerencie chats compartilhados anteriormente a qualquer momento via Configurações.', + com_ui_share_update_message: + 'Seu nome, instruções personalizadas e quaisquer mensagens que você adicionar após o compartilhamento permanecerão privadas.', + com_ui_share_updated_message: + 'Um link compartilhado para o seu chat foi atualizado. Gerencie chats compartilhados anteriormente a qualquer momento via Configurações.', com_ui_shared_link_not_found: 'Link compartilhado não encontrado', com_ui_delete_conversation: 'Excluir chat?', com_ui_delete_confirm: 'Isso excluirá', @@ -291,8 +325,10 @@ export default { com_ui_delete_tool_confirm: 'Tem certeza de que deseja excluir esta ferramenta?', com_ui_delete_action: 'Excluir Ação', com_ui_delete_action_confirm: 'Tem certeza de que deseja excluir esta ação?', - com_ui_delete_confirm_prompt_version_var: 'Isso excluirá a versão selecionada para "{0}". Se não houver outras versões, o prompt será excluído.', - com_ui_delete_assistant_confirm: 'Tem certeza de que deseja excluir este Assistente? Isso não pode ser desfeito.', + com_ui_delete_confirm_prompt_version_var: + 'Isso excluirá a versão selecionada para "{0}". Se não houver outras versões, o prompt será excluído.', + com_ui_delete_assistant_confirm: + 'Tem certeza de que deseja excluir este Assistente? Isso não pode ser desfeito.', com_ui_rename: 'Renomear', com_ui_archive: 'Arquivar', com_ui_archive_error: 'Falha ao arquivar conversa', @@ -303,7 +339,8 @@ export default { com_ui_upload: 'Carregar', com_ui_connect: 'Conectar', com_ui_locked: 'Bloqueado', - com_ui_upload_delay: 'O upload de "{0}" está demorando mais do que o esperado. Por favor, aguarde enquanto o arquivo termina de ser indexado para recuperação.', + com_ui_upload_delay: + 'O upload de "{0}" está demorando mais do que o esperado. Por favor, aguarde enquanto o arquivo termina de ser indexado para recuperação.', com_ui_privacy_policy: 'Política de Privacidade', com_ui_terms_of_service: 'Termos de Serviço', com_ui_use_micrphone: 'Usar microfone', @@ -324,13 +361,19 @@ export default { com_ui_bookmarks_delete_error: 'Houve um erro ao excluir o favorito', com_ui_bookmarks_add_to_conversation: 'Adicionar à conversa atual', com_ui_bookmarks_filter: 'Filtrar favoritos...', - com_ui_no_bookmarks: 'Parece que você ainda não tem favoritos. Clique em um chat e adicione um novo', + com_ui_no_bookmarks: + 'Parece que você ainda não tem favoritos. Clique em um chat e adicione um novo', com_ui_no_conversation_id: 'Nenhum ID de conversa encontrado', - com_auth_error_login: 'Não foi possível fazer login com as informações fornecidas. Por favor, verifique suas credenciais e tente novamente.', - com_auth_error_login_rl: 'Muitas tentativas de login em um curto período de tempo. Por favor, tente novamente mais tarde.', - com_auth_error_login_ban: 'Sua conta foi temporariamente banida devido a violações do nosso serviço.', - com_auth_error_login_server: 'Houve um erro interno no servidor. Por favor, aguarde alguns momentos e tente novamente.', - com_auth_error_login_unverified: 'Sua conta não foi verificada. Por favor, verifique seu e-mail para um link de verificação.', + com_auth_error_login: + 'Não foi possível fazer login com as informações fornecidas. Por favor, verifique suas credenciais e tente novamente.', + com_auth_error_login_rl: + 'Muitas tentativas de login em um curto período de tempo. Por favor, tente novamente mais tarde.', + com_auth_error_login_ban: + 'Sua conta foi temporariamente banida devido a violações do nosso serviço.', + com_auth_error_login_server: + 'Houve um erro interno no servidor. Por favor, aguarde alguns momentos e tente novamente.', + com_auth_error_login_unverified: + 'Sua conta não foi verificada. Por favor, verifique seu e-mail para um link de verificação.', com_auth_no_account: 'Não tem uma conta?', com_auth_sign_up: 'Inscrever-se', com_auth_sign_in: 'Entrar', @@ -365,14 +408,17 @@ export default { com_auth_already_have_account: 'Já tem uma conta?', com_auth_login: 'Entrar', com_auth_registration_success_insecure: 'Registro bem-sucedido.', - com_auth_registration_success_generic: 'Por favor, verifique seu e-mail para verificar seu endereço de e-mail.', + com_auth_registration_success_generic: + 'Por favor, verifique seu e-mail para verificar seu endereço de e-mail.', com_auth_reset_password: 'Redefinir sua senha', com_auth_click: 'Clique', com_auth_here: 'AQUI', com_auth_to_reset_your_password: 'para redefinir sua senha.', com_auth_reset_password_link_sent: 'E-mail enviado', - com_auth_reset_password_if_email_exists: 'Se uma conta com esse e-mail existir, um e-mail com instruções para redefinir a senha foi enviado. Certifique-se de verificar sua pasta de spam.', - com_auth_reset_password_email_sent: 'Se o usuário estiver registrado, um e-mail será enviado para a caixa de entrada.', + com_auth_reset_password_if_email_exists: + 'Se uma conta com esse e-mail existir, um e-mail com instruções para redefinir a senha foi enviado. Certifique-se de verificar sua pasta de spam.', + com_auth_reset_password_email_sent: + 'Se o usuário estiver registrado, um e-mail será enviado para a caixa de entrada.', com_auth_reset_password_success: 'Senha redefinida com sucesso', com_auth_login_with_new_password: 'Agora você pode fazer login com sua nova senha.', com_auth_error_invalid_reset_token: 'Este token de redefinição de senha não é mais válido.', @@ -382,7 +428,8 @@ export default { com_auth_welcome_back: 'Bem-vindo de volta', com_auth_back_to_login: 'Voltar para Login', com_auth_email_verification_failed: 'Falha na verificação de e-mail', - com_auth_email_verification_rate_limited: 'Muitas solicitações. Por favor, tente novamente mais tarde', + com_auth_email_verification_rate_limited: + 'Muitas solicitações. Por favor, tente novamente mais tarde', com_auth_email_verification_success: 'E-mail verificado com sucesso', com_auth_email_resent_success: 'E-mail de verificação reenviado com sucesso', com_auth_email_resent_failed: 'Falha ao reenviar e-mail de verificação', @@ -396,8 +443,10 @@ export default { com_endpoint_bing_enable_sydney: 'Habilitar Sydney', com_endpoint_bing_to_enable_sydney: 'Para habilitar Sydney', com_endpoint_bing_jailbreak: 'Jailbreak', - com_endpoint_bing_context_placeholder: 'O Bing pode usar até 7k tokens para "contexto", que pode referenciar para a conversa. O limite específico não é conhecido, mas pode ocorrer erros ao exceder 7k tokens', - com_endpoint_bing_system_message_placeholder: 'AVISO: O uso indevido deste recurso pode resultar em BANIMENTO do uso do Bing! Clique em "Mensagem do Sistema" para instruções completas e a mensagem padrão se omitida, que é o preset "Sydney" considerado seguro.', + com_endpoint_bing_context_placeholder: + 'O Bing pode usar até 7k tokens para "contexto", que pode referenciar para a conversa. O limite específico não é conhecido, mas pode ocorrer erros ao exceder 7k tokens', + com_endpoint_bing_system_message_placeholder: + 'AVISO: O uso indevido deste recurso pode resultar em BANIMENTO do uso do Bing! Clique em "Mensagem do Sistema" para instruções completas e a mensagem padrão se omitida, que é o preset "Sydney" considerado seguro.', com_endpoint_system_message: 'Mensagem do Sistema', com_endpoint_message: 'Mensagem', com_endpoint_message_not_appendable: 'Edite sua mensagem ou Regenerar.', @@ -411,15 +460,23 @@ export default { com_endpoint_token_count: 'Contagem de Tokens', com_endpoint_output: 'Saída', com_endpoint_context_tokens: 'Máximo de Tokens de Contexto', - com_endpoint_context_info: 'O número máximo de tokens que podem ser usados para contexto. Use isso para controlar quantos tokens são enviados por solicitação. Se não especificado, usará os padrões do sistema com base no tamanho do contexto dos modelos conhecidos. Definir valores mais altos pode resultar em erros e/ou maior custo de tokens.', - com_endpoint_google_temp: 'Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar isso ou Top P, mas não ambos.', - com_endpoint_google_topp: 'Top-p altera como o modelo seleciona tokens para saída. Os tokens são selecionados dos mais prováveis (veja o parâmetro topK) até os menos prováveis até que a soma de suas probabilidades atinja o valor top-p.', - com_endpoint_google_topk: 'Top-k altera como o modelo seleciona tokens para saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gananciosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).', - com_endpoint_google_maxoutputtokens: 'Número máximo de tokens que podem ser gerados na resposta. Especifique um valor mais baixo para respostas mais curtas e um valor mais alto para respostas mais longas. Nota: os modelos podem parar antes de atingir esse máximo.', + com_endpoint_context_info: + 'O número máximo de tokens que podem ser usados para contexto. Use isso para controlar quantos tokens são enviados por solicitação. Se não especificado, usará os padrões do sistema com base no tamanho do contexto dos modelos conhecidos. Definir valores mais altos pode resultar em erros e/ou maior custo de tokens.', + com_endpoint_google_temp: + 'Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar isso ou Top P, mas não ambos.', + com_endpoint_google_topp: + 'Top-p altera como o modelo seleciona tokens para saída. Os tokens são selecionados dos mais prováveis (veja o parâmetro topK) até os menos prováveis até que a soma de suas probabilidades atinja o valor top-p.', + com_endpoint_google_topk: + 'Top-k altera como o modelo seleciona tokens para saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gananciosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).', + com_endpoint_google_maxoutputtokens: + 'Número máximo de tokens que podem ser gerados na resposta. Especifique um valor mais baixo para respostas mais curtas e um valor mais alto para respostas mais longas. Nota: os modelos podem parar antes de atingir esse máximo.', com_endpoint_google_custom_name_placeholder: 'Defina um nome personalizado para o Google', - com_endpoint_prompt_prefix_placeholder: 'Defina instruções ou contexto personalizados. Ignorado se vazio.', - com_endpoint_instructions_assistants_placeholder: 'Substitui as instruções do assistente. Isso é útil para modificar o comportamento em uma base por execução.', - com_endpoint_prompt_prefix_assistants_placeholder: 'Defina instruções ou contexto adicionais além das instruções principais do Assistente. Ignorado se vazio.', + com_endpoint_prompt_prefix_placeholder: + 'Defina instruções ou contexto personalizados. Ignorado se vazio.', + com_endpoint_instructions_assistants_placeholder: + 'Substitui as instruções do assistente. Isso é útil para modificar o comportamento em uma base por execução.', + com_endpoint_prompt_prefix_assistants_placeholder: + 'Defina instruções ou contexto adicionais além das instruções principais do Assistente. Ignorado se vazio.', com_endpoint_custom_name: 'Nome Personalizado', com_endpoint_prompt_prefix: 'Instruções Personalizadas', com_endpoint_prompt_prefix_assistants: 'Instruções Adicionais', @@ -431,23 +488,38 @@ export default { com_endpoint_max_output_tokens: 'Máximo de Tokens de Saída', com_endpoint_stop: 'Sequências de Parada', com_endpoint_stop_placeholder: 'Separe os valores pressionando `Enter`', - com_endpoint_openai_max_tokens: 'Campo opcional `max_tokens`, representando o número máximo de tokens que podem ser gerados na conclusão do chat. O comprimento total dos tokens de entrada e dos tokens gerados é limitado pelo comprimento do contexto dos modelos. Você pode experimentar erros se esse número exceder o máximo de tokens de contexto.', - com_endpoint_openai_temp: 'Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar isso ou Top P, mas não ambos.', - com_endpoint_openai_max: 'O máximo de tokens para gerar. O comprimento total dos tokens de entrada e dos tokens gerados é limitado pelo comprimento do contexto do modelo.', - com_endpoint_openai_topp: 'Uma alternativa à amostragem com temperatura, chamada amostragem de núcleo, onde o modelo considera os resultados dos tokens com massa de probabilidade top_p. Então, 0.1 significa que apenas os tokens que compreendem os 10% principais da massa de probabilidade são considerados. Recomendamos alterar isso ou a temperatura, mas não ambos.', - com_endpoint_openai_freq: 'Número entre -2.0 e 2.0. Valores positivos penalizam novos tokens com base em sua frequência existente no texto até agora, diminuindo a probabilidade do modelo de repetir a mesma linha literalmente.', - com_endpoint_openai_pres: 'Número entre -2.0 e 2.0. Valores positivos penalizam novos tokens com base em sua presença no texto até agora, aumentando a probabilidade do modelo de falar sobre novos tópicos.', - com_endpoint_openai_resend: 'Reenviar todas as imagens anexadas anteriormente. Nota: isso pode aumentar significativamente o custo de tokens e você pode experimentar erros com muitos anexos de imagem.', - com_endpoint_openai_resend_files: 'Reenviar todos os arquivos anexados anteriormente. Nota: isso aumentará o custo de tokens e você pode experimentar erros com muitos anexos.', - com_endpoint_openai_detail: 'A resolução para solicitações de Visão. "Baixa" é mais barata e rápida, "Alta" é mais detalhada e cara, e "Auto" escolherá automaticamente entre as duas com base na resolução da imagem.', + com_endpoint_openai_max_tokens: + 'Campo opcional `max_tokens`, representando o número máximo de tokens que podem ser gerados na conclusão do chat. O comprimento total dos tokens de entrada e dos tokens gerados é limitado pelo comprimento do contexto dos modelos. Você pode experimentar erros se esse número exceder o máximo de tokens de contexto.', + com_endpoint_openai_temp: + 'Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar isso ou Top P, mas não ambos.', + com_endpoint_openai_max: + 'O máximo de tokens para gerar. O comprimento total dos tokens de entrada e dos tokens gerados é limitado pelo comprimento do contexto do modelo.', + com_endpoint_openai_topp: + 'Uma alternativa à amostragem com temperatura, chamada amostragem de núcleo, onde o modelo considera os resultados dos tokens com massa de probabilidade top_p. Então, 0.1 significa que apenas os tokens que compreendem os 10% principais da massa de probabilidade são considerados. Recomendamos alterar isso ou a temperatura, mas não ambos.', + com_endpoint_openai_freq: + 'Número entre -2.0 e 2.0. Valores positivos penalizam novos tokens com base em sua frequência existente no texto até agora, diminuindo a probabilidade do modelo de repetir a mesma linha literalmente.', + com_endpoint_openai_pres: + 'Número entre -2.0 e 2.0. Valores positivos penalizam novos tokens com base em sua presença no texto até agora, aumentando a probabilidade do modelo de falar sobre novos tópicos.', + com_endpoint_openai_resend: + 'Reenviar todas as imagens anexadas anteriormente. Nota: isso pode aumentar significativamente o custo de tokens e você pode experimentar erros com muitos anexos de imagem.', + com_endpoint_openai_resend_files: + 'Reenviar todos os arquivos anexados anteriormente. Nota: isso aumentará o custo de tokens e você pode experimentar erros com muitos anexos.', + com_endpoint_openai_detail: + 'A resolução para solicitações de Visão. "Baixa" é mais barata e rápida, "Alta" é mais detalhada e cara, e "Auto" escolherá automaticamente entre as duas com base na resolução da imagem.', com_endpoint_openai_stop: 'Até 4 sequências onde a API parará de gerar mais tokens.', com_endpoint_openai_custom_name_placeholder: 'Defina um nome personalizado para a IA', - com_endpoint_openai_prompt_prefix_placeholder: 'Defina instruções personalizadas para incluir na Mensagem do Sistema. Padrão: nenhuma', - com_endpoint_anthropic_temp: 'Varia de 0 a 1. Use temperatura mais próxima de 0 para tarefas analíticas / de múltipla escolha, e mais próxima de 1 para tarefas criativas e generativas. Recomendamos alterar isso ou Top P, mas não ambos.', - com_endpoint_anthropic_topp: 'Top-p altera como o modelo seleciona tokens para saída. Os tokens são selecionados dos mais prováveis (veja o parâmetro topK) até os menos prováveis até que a soma de suas probabilidades atinja o valor top-p.', - com_endpoint_anthropic_topk: 'Top-k altera como o modelo seleciona tokens para saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gananciosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).', - com_endpoint_anthropic_maxoutputtokens: 'Número máximo de tokens que podem ser gerados na resposta. Especifique um valor mais baixo para respostas mais curtas e um valor mais alto para respostas mais longas. Nota: os modelos podem parar antes de atingir esse máximo.', - com_endpoint_anthropic_prompt_cache: 'O cache de prompt permite reutilizar um grande contexto ou instruções em chamadas de API, reduzindo custos e latência', + com_endpoint_openai_prompt_prefix_placeholder: + 'Defina instruções personalizadas para incluir na Mensagem do Sistema. Padrão: nenhuma', + com_endpoint_anthropic_temp: + 'Varia de 0 a 1. Use temperatura mais próxima de 0 para tarefas analíticas / de múltipla escolha, e mais próxima de 1 para tarefas criativas e generativas. Recomendamos alterar isso ou Top P, mas não ambos.', + com_endpoint_anthropic_topp: + 'Top-p altera como o modelo seleciona tokens para saída. Os tokens são selecionados dos mais prováveis (veja o parâmetro topK) até os menos prováveis até que a soma de suas probabilidades atinja o valor top-p.', + com_endpoint_anthropic_topk: + 'Top-k altera como o modelo seleciona tokens para saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gananciosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).', + com_endpoint_anthropic_maxoutputtokens: + 'Número máximo de tokens que podem ser gerados na resposta. Especifique um valor mais baixo para respostas mais curtas e um valor mais alto para respostas mais longas. Nota: os modelos podem parar antes de atingir esse máximo.', + com_endpoint_anthropic_prompt_cache: + 'O cache de prompt permite reutilizar um grande contexto ou instruções em chamadas de API, reduzindo custos e latência', com_endpoint_prompt_cache: 'Usar Cache de Prompt', com_endpoint_anthropic_custom_name_placeholder: 'Defina um nome personalizado para Anthropic', com_endpoint_frequency_penalty: 'Penalidade de Frequência', @@ -459,15 +531,19 @@ export default { com_endpoint_plug_skip_completion: 'Pular Conclusão', com_endpoint_disabled_with_tools: 'desativado com ferramentas', com_endpoint_disabled_with_tools_placeholder: 'Desativado com Ferramentas Selecionadas', - com_endpoint_plug_set_custom_instructions_for_gpt_placeholder: 'Defina instruções personalizadas para incluir na Mensagem do Sistema. Padrão: nenhuma', + com_endpoint_plug_set_custom_instructions_for_gpt_placeholder: + 'Defina instruções personalizadas para incluir na Mensagem do Sistema. Padrão: nenhuma', com_endpoint_import: 'Importar', - com_endpoint_set_custom_name: 'Defina um nome personalizado, caso você possa encontrar este preset', + com_endpoint_set_custom_name: + 'Defina um nome personalizado, caso você possa encontrar este preset', com_endpoint_preset_delete_confirm: 'Tem certeza de que deseja excluir este preset?', com_endpoint_preset_clear_all_confirm: 'Tem certeza de que deseja excluir todos os seus presets?', com_endpoint_preset_import: 'Preset Importado!', - com_endpoint_preset_import_error: 'Houve um erro ao importar seu preset. Por favor, tente novamente.', + com_endpoint_preset_import_error: + 'Houve um erro ao importar seu preset. Por favor, tente novamente.', com_endpoint_preset_save_error: 'Houve um erro ao salvar seu preset. Por favor, tente novamente.', - com_endpoint_preset_delete_error: 'Houve um erro ao excluir seu preset. Por favor, tente novamente.', + com_endpoint_preset_delete_error: + 'Houve um erro ao excluir seu preset. Por favor, tente novamente.', com_endpoint_preset_default_removed: 'não é mais o preset padrão.', com_endpoint_preset_default_item: 'Padrão:', com_endpoint_preset_default_none: 'Nenhum preset padrão ativo.', @@ -493,7 +569,8 @@ export default { com_endpoint_use_active_assistant: 'Usar Assistente Ativo', com_endpoint_assistant_model: 'Modelo de Assistente', com_endpoint_save_as_preset: 'Salvar Como Preset', - com_endpoint_presets_clear_warning: 'Tem certeza de que deseja limpar todos os presets? Isso é irreversível.', + com_endpoint_presets_clear_warning: + 'Tem certeza de que deseja limpar todos os presets? Isso é irreversível.', com_endpoint_not_implemented: 'Não implementado', com_endpoint_no_presets: 'Ainda não há presets, use o botão de configurações para criar um', com_endpoint_not_available: 'Nenhum endpoint disponível', @@ -503,9 +580,11 @@ export default { com_endpoint_agent_model: 'Modelo de Agente (Recomendado: GPT-3.5)', com_endpoint_completion_model: 'Modelo de Conclusão (Recomendado: GPT-4)', com_endpoint_func_hover: 'Habilitar uso de Plugins como Funções OpenAI', - com_endpoint_skip_hover: 'Habilitar pular a etapa de conclusão, que revisa a resposta final e os passos gerados', + com_endpoint_skip_hover: + 'Habilitar pular a etapa de conclusão, que revisa a resposta final e os passos gerados', com_endpoint_config_key: 'Definir Chave API', - com_endpoint_assistant_placeholder: 'Por favor, selecione um Assistente no Painel Lateral Direito', + com_endpoint_assistant_placeholder: + 'Por favor, selecione um Assistente no Painel Lateral Direito', com_endpoint_config_placeholder: 'Defina sua Chave no menu do Cabeçalho para conversar.', com_endpoint_config_key_for: 'Definir Chave API para', com_endpoint_config_key_name: 'Chave', @@ -519,22 +598,28 @@ export default { com_endpoint_config_google_cloud_platform: '(do Google Cloud Platform)', com_endpoint_config_google_api_key: 'Chave API do Google', com_endpoint_config_google_gemini_api: '(API Gemini)', - com_endpoint_config_google_api_info: 'Para obter sua chave API de Linguagem Generativa (para Gemini),', + com_endpoint_config_google_api_info: + 'Para obter sua chave API de Linguagem Generativa (para Gemini),', com_endpoint_config_key_import_json_key: 'Importar Chave JSON da Conta de Serviço.', - com_endpoint_config_key_import_json_key_success: 'Chave JSON da Conta de Serviço Importada com Sucesso', - com_endpoint_config_key_import_json_key_invalid: 'Chave JSON da Conta de Serviço Inválida, Você importou o arquivo correto?', + com_endpoint_config_key_import_json_key_success: + 'Chave JSON da Conta de Serviço Importada com Sucesso', + com_endpoint_config_key_import_json_key_invalid: + 'Chave JSON da Conta de Serviço Inválida, Você importou o arquivo correto?', com_endpoint_config_key_get_edge_key: 'Para obter seu token de acesso para o Bing, faça login em', - com_endpoint_config_key_get_edge_key_dev_tool: 'Use ferramentas de desenvolvedor ou uma extensão enquanto estiver logado no site para copiar o conteúdo do cookie _U. Se isso falhar, siga estas', + com_endpoint_config_key_get_edge_key_dev_tool: + 'Use ferramentas de desenvolvedor ou uma extensão enquanto estiver logado no site para copiar o conteúdo do cookie _U. Se isso falhar, siga estas', com_endpoint_config_key_edge_instructions: 'instruções', com_endpoint_config_key_edge_full_key_string: 'para fornecer as strings completas do cookie.', - com_endpoint_config_key_chatgpt: 'Para obter seu token de acesso para o ChatGPT "Versão Gratuita", faça login em', + com_endpoint_config_key_chatgpt: + 'Para obter seu token de acesso para o ChatGPT "Versão Gratuita", faça login em', com_endpoint_config_key_chatgpt_then_visit: 'depois visite', com_endpoint_config_key_chatgpt_copy_token: 'Copiar token de acesso.', com_endpoint_config_key_google_need_to: 'Você precisa', com_endpoint_config_key_google_vertex_ai: 'Habilitar Vertex AI', com_endpoint_config_key_google_vertex_api: 'API no Google Cloud, então', com_endpoint_config_key_google_service_account: 'Criar uma Conta de Serviço', - com_endpoint_config_key_google_vertex_api_role: 'Certifique-se de clicar em "Criar e Continuar" para dar pelo menos o papel de "Usuário do Vertex AI". Por fim, crie uma chave JSON para importar aqui.', + com_endpoint_config_key_google_vertex_api_role: + 'Certifique-se de clicar em "Criar e Continuar" para dar pelo menos o papel de "Usuário do Vertex AI". Por fim, crie uma chave JSON para importar aqui.', com_nav_account_settings: 'Configurações da Conta', com_nav_font_size: 'Tamanho da Fonte da Mensagem', com_nav_font_size_xs: 'Extra Pequeno', @@ -573,14 +658,16 @@ export default { com_ui_upload_image: 'Carregar uma imagem', com_ui_select_a_category: 'Nenhuma categoria selecionada', com_ui_clear_all: 'Limpar tudo', - com_nav_tool_dialog_description: 'O assistente deve ser salvo para persistir as seleções de ferramentas.', + com_nav_tool_dialog_description: + 'O assistente deve ser salvo para persistir as seleções de ferramentas.', com_show_agent_settings: 'Mostrar Configurações do Agente', com_show_completion_settings: 'Mostrar Configurações de Conclusão', com_hide_examples: 'Ocultar Exemplos', com_show_examples: 'Mostrar Exemplos', com_nav_plugin_search: 'Buscar plugins', com_nav_tool_search: 'Buscar ferramentas', - com_nav_plugin_auth_error: 'Houve um erro ao tentar autenticar este plugin. Por favor, tente novamente.', + com_nav_plugin_auth_error: + 'Houve um erro ao tentar autenticar este plugin. Por favor, tente novamente.', com_nav_export_filename: 'Nome do arquivo', com_nav_export_filename_placeholder: 'Definir o nome do arquivo', com_nav_export_type: 'Tipo', @@ -625,7 +712,8 @@ export default { com_nav_archive_name: 'Nome', com_nav_archive_created_at: 'Data de Arquivamento', com_nav_clear_conversation: 'Limpar conversas', - com_nav_clear_conversation_confirm_message: 'Tem certeza de que deseja limpar todas as conversas? Isso é irreversível.', + com_nav_clear_conversation_confirm_message: + 'Tem certeza de que deseja limpar todas as conversas? Isso é irreversível.', com_nav_help_faq: 'Ajuda & FAQ', com_nav_settings: 'Configurações', com_nav_search_placeholder: 'Buscar mensagens', @@ -646,12 +734,18 @@ export default { com_nav_audio_process_error: 'Erro ao processar áudio: {0}', com_nav_long_audio_warning: 'Textos mais longos levarão mais tempo para processar.', com_nav_tts_init_error: 'Falha ao inicializar texto-para-fala: {0}', - com_nav_tts_unsupported_error: 'Texto-para-fala para o mecanismo selecionado não é suportado neste navegador.', - com_nav_source_buffer_error: 'Erro ao configurar a reprodução de áudio. Por favor, atualize a página.', - com_nav_media_source_init_error: 'Não foi possível preparar o reprodutor de áudio. Por favor, verifique as configurações do seu navegador.', - com_nav_buffer_append_error: 'Problema com o streaming de áudio. A reprodução pode ser interrompida.', - com_nav_speech_cancel_error: 'Não foi possível parar a reprodução de áudio. Você pode precisar atualizar a página.', - com_nav_voices_fetch_error: 'Não foi possível recuperar as opções de voz. Por favor, verifique sua conexão com a internet.', + com_nav_tts_unsupported_error: + 'Texto-para-fala para o mecanismo selecionado não é suportado neste navegador.', + com_nav_source_buffer_error: + 'Erro ao configurar a reprodução de áudio. Por favor, atualize a página.', + com_nav_media_source_init_error: + 'Não foi possível preparar o reprodutor de áudio. Por favor, verifique as configurações do seu navegador.', + com_nav_buffer_append_error: + 'Problema com o streaming de áudio. A reprodução pode ser interrompida.', + com_nav_speech_cancel_error: + 'Não foi possível parar a reprodução de áudio. Você pode precisar atualizar a página.', + com_nav_voices_fetch_error: + 'Não foi possível recuperar as opções de voz. Por favor, verifique sua conexão com a internet.', com_nav_engine: 'Motor', com_nav_browser: 'Navegador', com_nav_edge: 'Edge', @@ -660,20 +754,30 @@ export default { com_nav_enable_cache_tts: 'Habilitar cache TTS', com_nav_voice_select: 'Voz', com_nav_enable_cloud_browser_voice: 'Usar vozes baseadas na nuvem', - com_nav_info_enter_to_send: 'Quando habilitado, pressionar `ENTER` enviará sua mensagem. Quando desabilitado, pressionar Enter adicionará uma nova linha, e você precisará pressionar `CTRL + ENTER` para enviar sua mensagem.', - com_nav_info_save_draft: 'Quando habilitado, o texto e os anexos que você inserir no formulário de chat serão salvos automaticamente localmente como rascunhos. Esses rascunhos estarão disponíveis mesmo se você recarregar a página ou mudar para uma conversa diferente. Os rascunhos são armazenados localmente no seu dispositivo e são excluídos uma vez que a mensagem é enviada.', - com_nav_info_fork_change_default: '`Apenas mensagens visíveis` inclui apenas o caminho direto para a mensagem selecionada. `Incluir ramos relacionados` adiciona ramos ao longo do caminho. `Incluir tudo de/para aqui` inclui todas as mensagens e ramos conectados.', - com_nav_info_fork_split_target_setting: 'Quando habilitado, a bifurcação começará da mensagem alvo até a última mensagem na conversa, de acordo com o comportamento selecionado.', - com_nav_info_user_name_display: 'Quando habilitado, o nome de usuário do remetente será mostrado acima de cada mensagem que você enviar. Quando desabilitado, você verá apenas "Você" acima de suas mensagens.', - com_nav_info_latex_parsing: 'Quando habilitado, o código LaTeX nas mensagens será renderizado como equações matemáticas. Desabilitar isso pode melhorar o desempenho se você não precisar de renderização LaTeX.', - com_nav_info_revoke: 'Esta ação revogará e removerá todas as chaves de API que você forneceu. Você precisará reentrar essas credenciais para continuar usando esses endpoints.', - com_nav_info_delete_cache_storage: 'Esta ação excluirá todos os arquivos de áudio TTS (Texto-para-Fala) armazenados em cache no seu dispositivo. Arquivos de áudio em cache são usados para acelerar a reprodução de TTS gerado anteriormente, mas podem consumir espaço de armazenamento no seu dispositivo.', + com_nav_info_enter_to_send: + 'Quando habilitado, pressionar `ENTER` enviará sua mensagem. Quando desabilitado, pressionar Enter adicionará uma nova linha, e você precisará pressionar `CTRL + ENTER` para enviar sua mensagem.', + com_nav_info_save_draft: + 'Quando habilitado, o texto e os anexos que você inserir no formulário de chat serão salvos automaticamente localmente como rascunhos. Esses rascunhos estarão disponíveis mesmo se você recarregar a página ou mudar para uma conversa diferente. Os rascunhos são armazenados localmente no seu dispositivo e são excluídos uma vez que a mensagem é enviada.', + com_nav_info_fork_change_default: + '`Apenas mensagens visíveis` inclui apenas o caminho direto para a mensagem selecionada. `Incluir ramos relacionados` adiciona ramos ao longo do caminho. `Incluir tudo de/para aqui` inclui todas as mensagens e ramos conectados.', + com_nav_info_fork_split_target_setting: + 'Quando habilitado, a bifurcação começará da mensagem alvo até a última mensagem na conversa, de acordo com o comportamento selecionado.', + com_nav_info_user_name_display: + 'Quando habilitado, o nome de usuário do remetente será mostrado acima de cada mensagem que você enviar. Quando desabilitado, você verá apenas "Você" acima de suas mensagens.', + com_nav_info_latex_parsing: + 'Quando habilitado, o código LaTeX nas mensagens será renderizado como equações matemáticas. Desabilitar isso pode melhorar o desempenho se você não precisar de renderização LaTeX.', + com_nav_info_revoke: + 'Esta ação revogará e removerá todas as chaves de API que você forneceu. Você precisará reentrar essas credenciais para continuar usando esses endpoints.', + com_nav_info_delete_cache_storage: + 'Esta ação excluirá todos os arquivos de áudio TTS (Texto-para-Fala) armazenados em cache no seu dispositivo. Arquivos de áudio em cache são usados para acelerar a reprodução de TTS gerado anteriormente, mas podem consumir espaço de armazenamento no seu dispositivo.', com_nav_commands: 'Comandos', com_nav_commands_tab: 'Configurações de Comando', com_nav_at_command: 'Comando @', - com_nav_at_command_description: 'Alternar comando "@" para alternar endpoints, modelos, predefinições, etc.', + com_nav_at_command_description: + 'Alternar comando "@" para alternar endpoints, modelos, predefinições, etc.', com_nav_plus_command: 'Comando +', - com_nav_plus_command_description: 'Alternar comando "+" para adicionar uma configuração de resposta múltipla', + com_nav_plus_command_description: + 'Alternar comando "+" para adicionar uma configuração de resposta múltipla', com_nav_slash_command: 'Comando /', com_nav_slash_command_description: 'Alternar comando "/" para selecionar um prompt via teclado', com_nav_command_settings: 'Configurações de Comando', @@ -770,7 +874,7 @@ export const comparisons = { english: 'Search assistants by name', translated: 'Pesquisar assistentes por nome', }, - com_assistants_tools: { + com_ui_tools: { english: 'Tools', translated: 'Ferramentas', }, diff --git a/client/src/localization/languages/De.ts b/client/src/localization/languages/De.ts index eeaa70b450..99c8b87c98 100644 --- a/client/src/localization/languages/De.ts +++ b/client/src/localization/languages/De.ts @@ -38,7 +38,7 @@ export default { com_assistants_code_interpreter_files: 'Die folgenden Dateien sind nur für den Code-Interpreter:', com_assistants_retrieval: 'Abruf', com_assistants_search_name: 'Assistenten nach Namen suchen', - com_assistants_tools: 'Werkzeuge', + com_ui_tools: 'Werkzeuge', com_assistants_actions: 'Aktionen', com_assistants_add_tools: 'Werkzeuge hinzufügen', com_assistants_add_actions: 'Aktionen hinzufügen', @@ -840,7 +840,7 @@ export const comparisons = { english: 'Search assistants by name', translated: 'Assistenten nach Namen suchen', }, - com_assistants_tools: { + com_ui_tools: { english: 'Tools', translated: 'Werkzeuge', }, diff --git a/client/src/localization/languages/Eng.ts b/client/src/localization/languages/Eng.ts index 6a6617cc52..77371333f7 100644 --- a/client/src/localization/languages/Eng.ts +++ b/client/src/localization/languages/Eng.ts @@ -35,6 +35,10 @@ export default { 'The latest message token count is too long, exceeding the token limit ({0} respectively). Please shorten your message, adjust the max context size from the conversation parameters, or fork the conversation to continue.', com_files_no_results: 'No results.', com_files_filter: 'Filter files...', + com_generated_files: 'Generated files:', + com_download_expired: '(download expired)', + com_download_expires: '(click here to download - expires {0})', + com_click_to_download: '(click here to download)', com_files_number_selected: '{0} of {1} file(s) selected', com_sidepanel_select_assistant: 'Select an Assistant', com_sidepanel_parameters: 'Parameters', @@ -59,7 +63,7 @@ export default { com_assistants_code_interpreter_files: 'Files below are for Code Interpreter only:', com_assistants_retrieval: 'Retrieval', com_assistants_search_name: 'Search assistants by name', - com_assistants_tools: 'Tools', + com_ui_tools: 'Tools', com_assistants_actions: 'Actions', com_assistants_add_tools: 'Add Tools', com_assistants_add_actions: 'Add Actions', @@ -93,6 +97,18 @@ export default { com_agents_search_name: 'Search agents by name', com_agents_update_error: 'There was an error updating your agent.', com_agents_create_error: 'There was an error creating your agent.', + com_agents_missing_provider_model: 'Please select a provider and model before creating an agent.', + com_agents_allow_editing: 'Allow other users to edit your agent', + com_agents_not_available: 'Agent Not Available', + com_agents_no_access: 'You don\'t have access to edit this agent.', + com_agents_enable_file_search: 'Enable File Search', + com_agents_file_search_info: + 'When enabled, the agent will be informed of the exact filenames listed below, allowing it to retrieve relevant context from these files.', + com_agents_file_search_disabled: 'Agent must be created before uploading files for File Search.', + com_agents_execute_code: 'Run Code', + com_ui_agent_already_shared_to_all: 'This agent is already shared to all users', + com_ui_agent_editing_allowed: 'Other users can already edit this agent', + com_ui_no_changes: 'No changes to update', com_ui_date_today: 'Today', com_ui_date_yesterday: 'Yesterday', com_ui_date_previous_7_days: 'Previous 7 days', @@ -639,6 +655,7 @@ export default { com_nav_plugin_uninstall: 'Uninstall', com_ui_add: 'Add', com_nav_tool_remove: 'Remove', + com_nav_tool_dialog_agents: 'Agent Tools', com_nav_tool_dialog: 'Assistant Tools', com_ui_misc: 'Misc.', com_ui_roleplay: 'Roleplay', diff --git a/client/src/localization/languages/Es.ts b/client/src/localization/languages/Es.ts index 375e2cfdeb..ba2b7643c8 100644 --- a/client/src/localization/languages/Es.ts +++ b/client/src/localization/languages/Es.ts @@ -24,7 +24,7 @@ export default { 'Los siguientes archivos solo están disponibles para el Intérprete de Código:', com_assistants_retrieval: 'Recuperación', com_assistants_search_name: 'Buscar asistentes por nombre', - com_assistants_tools: 'Herramientas', + com_ui_tools: 'Herramientas', com_assistants_actions: 'Acciones', com_assistants_add_tools: 'Añadir Herramientas', com_assistants_add_actions: 'Añadir Acciones', @@ -697,7 +697,7 @@ export const comparisons = { english: 'Search assistants by name', translated: 'Buscar asistentes por nombre', }, - com_assistants_tools: { + com_ui_tools: { english: 'Tools', translated: 'Herramientas', }, diff --git a/client/src/localization/languages/Fi.ts b/client/src/localization/languages/Fi.ts index 02a8bba63c..8737fc5c03 100644 --- a/client/src/localization/languages/Fi.ts +++ b/client/src/localization/languages/Fi.ts @@ -33,7 +33,7 @@ export default { com_assistants_code_interpreter_files: 'Seuraavat tiedostot ovat vain Kooditulkin käytettävissä:', com_assistants_retrieval: 'Tiedonhaku', com_assistants_search_name: 'Hae Avustajia nimen perusteella', - com_assistants_tools: 'Työkalut', + com_ui_tools: 'Työkalut', com_assistants_actions: 'Toiminnot', com_assistants_add_tools: 'Lisää Työkaluja', com_assistants_add_actions: 'Lisää Toimintoja', diff --git a/client/src/localization/languages/Fr.ts b/client/src/localization/languages/Fr.ts index 29a602aef5..063e6b0c0d 100644 --- a/client/src/localization/languages/Fr.ts +++ b/client/src/localization/languages/Fr.ts @@ -483,7 +483,7 @@ export default { 'Les fichiers suivants sont disponibles uniquement pour l\'interpréteur de code :', com_assistants_retrieval: 'Récupération', com_assistants_search_name: 'Rechercher des assistants par nom', - com_assistants_tools: 'Outils', + com_ui_tools: 'Outils', com_assistants_actions: 'Actions', com_assistants_add_tools: 'Ajouter des outils', com_assistants_add_actions: 'Ajouter des actions', @@ -2434,7 +2434,7 @@ export const comparisons = { english: 'Search assistants by name', translated: 'Rechercher des assistants par nom', }, - com_assistants_tools: { + com_ui_tools: { english: 'Tools', translated: 'Outils', }, diff --git a/client/src/localization/languages/It.ts b/client/src/localization/languages/It.ts index b23cbb5a59..a4692adca2 100644 --- a/client/src/localization/languages/It.ts +++ b/client/src/localization/languages/It.ts @@ -32,7 +32,7 @@ export default { 'I seguenti file sono disponibili solo per Code Interpreter:', com_assistants_retrieval: 'Retrival', com_assistants_search_name: 'Cerca assistenti per nome', - com_assistants_tools: 'Strumenti', + com_ui_tools: 'Strumenti', com_assistants_actions: 'Azioni', com_assistants_add_tools: 'Aggiungi Strumenti', com_assistants_add_actions: 'Aggiungi Azioni', @@ -727,7 +727,7 @@ export const comparisons = { english: 'Search assistants by name', translated: 'Cerca assistenti per nome', }, - com_assistants_tools: { + com_ui_tools: { english: 'Tools', translated: 'Strumenti', }, diff --git a/client/src/localization/languages/Jp.ts b/client/src/localization/languages/Jp.ts index 3c13aa8c43..69d8184f74 100644 --- a/client/src/localization/languages/Jp.ts +++ b/client/src/localization/languages/Jp.ts @@ -6,8 +6,7 @@ export default { com_nav_convo_menu_options: '会話メニューオプション', com_ui_artifacts: 'アーティファクト', com_ui_artifacts_toggle: 'アーティファクト UI の切替', - com_nav_info_code_artifacts: - 'チャットの横に実験的なコード アーティファクトの表示を有効にします', + com_nav_info_code_artifacts: 'チャットの横に実験的なコード アーティファクトの表示を有効にします', com_ui_include_shadcnui: 'shadcn/uiコンポーネントの指示を含める', com_nav_info_include_shadcnui: '有効にすると、shadcn/uiコンポーネントを使用するための指示が含まれます。shadcn/uiはRadix UIとTailwind CSSを使用して構築された再利用可能なコンポーネントのコレクションです。注:これらの指示は長文ですので、LLM に正しいインポートとコンポーネントを知らせることが重要でない限り、有効にしないでください。これらのコンポーネントの詳細については、https://ui.shadcn.com/をご覧ください。', @@ -53,7 +52,7 @@ export default { com_assistants_code_interpreter_files: '次のファイルはコードインタプリタでのみ使用できます。', com_assistants_retrieval: '検索', com_assistants_search_name: 'アシスタントの名前で検索', - com_assistants_tools: 'ツール', + com_ui_tools: 'ツール', com_assistants_actions: 'アクション', com_assistants_add_tools: 'ツールを追加', com_assistants_add_actions: 'アクションを追加', @@ -69,7 +68,8 @@ export default { com_assistants_update_actions_success: 'アクションが作成または更新されました', com_assistants_update_actions_error: 'アクションの作成または更新中にエラーが発生しました。', com_assistants_delete_actions_error: 'アクションの削除中にエラーが発生しました。', - com_assistants_actions_info: 'アシスタントが API を介して情報を取得したり、アクションを実行したりできるようにします\'s', + com_assistants_actions_info: + 'アシスタントが API を介して情報を取得したり、アクションを実行したりできるようにします\'s', com_assistants_name_placeholder: 'オプション: アシスタントの名前', com_assistants_instructions_placeholder: 'アシスタントが使用するシステム指示', com_assistants_description_placeholder: 'オプション: ここにアシスタントについて説明します', @@ -104,10 +104,12 @@ export default { com_ui_date_november: '11月', com_ui_date_december: '12月', com_ui_field_required: '必須入力項目です', - com_ui_download_error: 'ファイルのダウンロード中にエラーが発生しました。ファイルが削除された可能性があります。', + com_ui_download_error: + 'ファイルのダウンロード中にエラーが発生しました。ファイルが削除された可能性があります。', com_ui_attach_error_type: 'エンドポイントでサポートされていないファイルタイプ:', com_ui_attach_error_openai: '他のエンドポイントにアシスタントファイルを添付することはできません', - com_ui_attach_warn_endpoint: '互換性のあるツールがない場合、非アシスタントのファイルは無視される可能性があります', + com_ui_attach_warn_endpoint: + '互換性のあるツールがない場合、非アシスタントのファイルは無視される可能性があります', com_ui_attach_error_size: 'エンドポイントのファイルサイズ制限を超えました:', com_ui_attach_error: 'ファイルを添付できません。会話を作成または選択するか、ページを更新してみてください。', @@ -193,8 +195,10 @@ export default { com_ui_none_selected: '選択されていません', com_ui_upload_success: 'アップロード成功', com_ui_upload_error: 'ファイルのアップロード中にエラーが発生しました。', - com_ui_upload_invalid: 'アップロードに無効なファイルです。制限を超えない画像である必要があります。', - com_ui_upload_invalid_var: 'アップロードに無効なファイルです。 {0} MBまでの画像である必要があります。', + com_ui_upload_invalid: + 'アップロードに無効なファイルです。制限を超えない画像である必要があります。', + com_ui_upload_invalid_var: + 'アップロードに無効なファイルです。 {0} MBまでの画像である必要があります。', com_ui_cancel: 'キャンセル', com_ui_save: '保存', com_ui_renaming_var: '改名 "{0}"', @@ -235,7 +239,8 @@ export default { com_ui_fork_branches: '関連ブランチを含める', com_ui_fork_visible: '表示メッセージのみ', com_ui_fork_from_message: '分岐オプションを選択する', - com_ui_mention: 'エンドポイント、アシスタント、またはプリセットを素早く切り替えるには、それらを言及してください。', + com_ui_mention: + 'エンドポイント、アシスタント、またはプリセットを素早く切り替えるには、それらを言及してください。', com_ui_add_model_preset: '追加の応答のためのモデルまたはプリセットを追加する', com_assistants_max_starters_reached: '会話の開始が最大数に達しました', com_ui_regenerate: '再度 生成する', @@ -303,7 +308,8 @@ export default { com_ui_share_error: 'チャットの共有リンクの共有中にエラーが発生しました', com_ui_share_retrieve_error: '共有リンクの削除中にエラーが発生しました。', com_ui_share_delete_error: '共有リンクの削除中にエラーが発生しました。', - com_ui_share_create_message: 'あなたの名前と共有リンクを作成した後のメッセージは、共有されません。', + com_ui_share_create_message: + 'あなたの名前と共有リンクを作成した後のメッセージは、共有されません。', com_ui_share_created_message: 'チャットの共有リンクが作成されました。設定から以前共有したチャットを管理できます。', com_ui_share_update_message: @@ -319,8 +325,7 @@ export default { com_ui_delete_action_confirm: 'このアクションを削除してもよろしいですか?', com_ui_delete_confirm_prompt_version_var: 'これは、選択されたバージョンを "{0}." から削除します。他のバージョンが存在しない場合、プロンプトが削除されます。', - com_ui_delete_assistant_confirm: - 'このアシスタントを削除しますか? この操作は元に戻せません。', + com_ui_delete_assistant_confirm: 'このアシスタントを削除しますか? この操作は元に戻せません。', com_ui_rename: 'タイトル変更', com_ui_archive: 'アーカイブ', com_ui_archive_error: 'アーカイブに失敗しました。', @@ -353,14 +358,14 @@ export default { com_ui_bookmarks_delete_error: 'ブックマークの削除中にエラーが発生しました', com_ui_bookmarks_add_to_conversation: '現在の会話に追加', com_ui_bookmarks_filter: 'ブックマークをフィルタリング...', - com_ui_no_bookmarks: 'ブックマークがまだないようです。チャットをクリックして新しいブックマークを追加してください', + com_ui_no_bookmarks: + 'ブックマークがまだないようです。チャットをクリックして新しいブックマークを追加してください', com_ui_no_conversation_id: '会話 ID が見つかりません', com_auth_error_login: '入力された情報ではログインできませんでした。認証情報を確認した上で再度お試しください。', com_auth_error_login_rl: 'お使いのIPアドレスから短時間に多数のログイン試行がありました。しばらくしてから再度お試しください。', - com_auth_error_login_ban: - '本サービスの利用規約違反のため、一時的にアカウントを停止しました。', + com_auth_error_login_ban: '本サービスの利用規約違反のため、一時的にアカウントを停止しました。', com_auth_error_login_server: 'サーバーエラーが発生しています。。しばらくしてから再度お試しください。', com_auth_error_login_unverified: @@ -387,8 +392,7 @@ export default { com_auth_password_not_match: 'パスワードが一致しません', com_auth_continue: '続ける', com_auth_create_account: 'アカウント登録', - com_auth_error_create: - 'アカウント登録に失敗しました。もう一度お試しください。', + com_auth_error_create: 'アカウント登録に失敗しました。もう一度お試しください。', com_auth_full_name: 'フルネーム', com_auth_name_required: 'フルネームは必須です', com_auth_name_min_length: 'フルネームは最低3文字で入力してください', @@ -408,8 +412,7 @@ export default { com_auth_reset_password_link_sent: 'メールを送信', com_auth_reset_password_if_email_exists: 'そのメールアドレスのアカウントが存在する場合は、パスワードリセット手順が記載されたメールが送信されています。スパムフォルダを必ず確認してください。', - com_auth_reset_password_email_sent: - 'パスワードのリセット方法を記載したメールを送信しました。', + com_auth_reset_password_email_sent: 'パスワードのリセット方法を記載したメールを送信しました。', com_auth_reset_password_success: 'パスワードのリセットに成功しました', com_auth_login_with_new_password: '新しいパスワードでログインをお試しください。', com_auth_error_invalid_reset_token: '無効なパスワードリセットトークンです。', @@ -419,7 +422,8 @@ export default { com_auth_welcome_back: 'おかえりなさい', com_auth_back_to_login: 'ログイン画面に戻る', com_auth_email_verification_failed: 'メール検証に失敗しました', - com_auth_email_verification_rate_limited: 'リクエストが多すぎます。しばらくしてからもう一度お試しください', + com_auth_email_verification_rate_limited: + 'リクエストが多すぎます。しばらくしてからもう一度お試しください', com_auth_email_verification_success: 'メールが正常に検証されました', com_auth_email_resent_success: '検証メールが正常に再送信されました', com_auth_email_resent_failed: '検証メールの再送信に失敗しました', @@ -461,7 +465,8 @@ export default { com_endpoint_google_maxoutputtokens: ' 生成されるレスポンスの最大トークン数。短いレスポンスには低い値を、長いレスポンスには高い値を指定します。', com_endpoint_google_custom_name_placeholder: 'Googleのカスタム名を設定する', - com_endpoint_prompt_prefix_placeholder: 'custom instructions か context を設定する。空の場合は無視されます。', + com_endpoint_prompt_prefix_placeholder: + 'custom instructions か context を設定する。空の場合は無視されます。', com_endpoint_instructions_assistants_placeholder: 'アシスタントの指示を上書きします。これは、実行ごとに動作を変更する場合に便利です。', com_endpoint_prompt_prefix_assistants_placeholder: @@ -496,7 +501,8 @@ export default { '以前に添付されたすべてのファイルを再送信します。注意:これにより、トークンのコストが増加し、多くの添付ファイルでエラーが発生する可能性があります。', com_endpoint_openai_detail: 'Visionリクエストの解像度を選択します。"Low"はコストが安くて低解像度、"Highは"コストが高くて高解像度"、"Auto"は画像の解像度に基づいて自動的に選択します。', - com_endpoint_openai_stop: 'APIがさらにトークンを生成するのを止めるため、最大で4つのシーケンスを設定可能', + com_endpoint_openai_stop: + 'APIがさらにトークンを生成するのを止めるため、最大で4つのシーケンスを設定可能', com_endpoint_openai_custom_name_placeholder: 'ChatGPTのカスタム名を設定する', com_endpoint_openai_prompt_prefix_placeholder: 'システムメッセージに含める Custom Instructions。デフォルト: none', @@ -586,15 +592,18 @@ export default { com_endpoint_config_google_gemini_api: '(Gemini API)', com_endpoint_config_google_api_info: 'Gemeni用のGenerative Language API keyを取得するには', com_endpoint_config_key_import_json_key: 'Service Account JSON Key をインポートする。', - com_endpoint_config_key_import_json_key_success: 'Service Account JSON Keyのインポートに成功しました。', + com_endpoint_config_key_import_json_key_success: + 'Service Account JSON Keyのインポートに成功しました。', com_endpoint_config_key_import_json_key_invalid: '無効なService Account JSON Keyです。正しいファイルかどうか確認してください。', - com_endpoint_config_key_get_edge_key: 'Bing用のアクセストークンを取得するためにログインをしてください: ', + com_endpoint_config_key_get_edge_key: + 'Bing用のアクセストークンを取得するためにログインをしてください: ', com_endpoint_config_key_get_edge_key_dev_tool: 'サイトにログインした状態で、開発ツールまたは拡張機能を使用して、_U クッキーの内容をコピーします。もし失敗する場合は次の手順に従ってください。', com_endpoint_config_key_edge_instructions: '手順', com_endpoint_config_key_edge_full_key_string: 'to provide the full cookie strings.', - com_endpoint_config_key_chatgpt: 'ChatGPTの「無料版」のアクセストークンを入手するためにへログインをしてください:', + com_endpoint_config_key_chatgpt: + 'ChatGPTの「無料版」のアクセストークンを入手するためにへログインをしてください:', com_endpoint_config_key_chatgpt_then_visit: 'つぎに、ここへアクセスしてください:', com_endpoint_config_key_chatgpt_copy_token: 'トークンをコピーしてください。', com_endpoint_config_key_google_need_to: 'こちらを有効化する必要があります:', @@ -615,7 +624,8 @@ export default { com_nav_auto_scroll: 'チャットを開いたときに最新まで自動でスクロール', com_nav_hide_panel: '右側のパネルを非表示', com_nav_modular_chat: '会話の途中でのエンドポイント切替を有効化', - com_nav_latex_parsing: 'メッセージ内の LaTeX の構文解析 (パフォーマンスに影響する可能性があります)', + com_nav_latex_parsing: + 'メッセージ内の LaTeX の構文解析 (パフォーマンスに影響する可能性があります)', com_nav_text_to_speech: 'テキスト読み上げ', com_nav_automatic_playback: '最新メッセージを自動再生', com_nav_speech_to_text: '音声テキスト変換', @@ -641,7 +651,8 @@ export default { com_ui_upload_image: '画像をアップロード', com_ui_select_a_category: 'カテゴリ未選択', com_ui_clear_all: 'すべてクリア', - com_nav_tool_dialog_description: 'ツールの選択を維持するには、アシスタントを保存する必要があります。', + com_nav_tool_dialog_description: + 'ツールの選択を維持するには、アシスタントを保存する必要があります。', com_show_agent_settings: 'エージェント設定を表示', com_show_completion_settings: 'コンプリーション設定を表示', com_hide_examples: '例を非表示', @@ -703,7 +714,8 @@ export default { com_nav_delete_account_confirm: 'アカウントを削除しますか?', com_nav_delete_account_button: 'アカウントを完全に削除する', com_nav_delete_account_email_placeholder: 'アカウントのメールアドレスを入力してください', - com_nav_delete_account_confirm_placeholder: '続行するには、以下の入力フィールドに「DELETE」と入力してください', + com_nav_delete_account_confirm_placeholder: + '続行するには、以下の入力フィールドに「DELETE」と入力してください', com_nav_delete_warning: '警告: この操作により、アカウントが完全に削除されます。', com_nav_delete_data_info: 'すべてのデータが削除されます。', com_nav_conversation_mode: '会話モード', @@ -718,11 +730,14 @@ export default { com_nav_tts_init_error: 'テキスト読み上げの初期化に失敗しました: {0}', com_nav_tts_unsupported_error: '選択したエンジンでのテキスト読み上げはこのブラウザではサポートされていません。', - com_nav_source_buffer_error: 'オーディオ再生の設定エラーが発生しました。ページを更新してください。', + com_nav_source_buffer_error: + 'オーディオ再生の設定エラーが発生しました。ページを更新してください。', com_nav_media_source_init_error: 'オーディオプレーヤーを準備できませんでした。ブラウザの設定を確認してください。', - com_nav_buffer_append_error: 'オーディオストリーミングに問題が発生しました。再生が中断される可能性があります。', - com_nav_speech_cancel_error: 'オーディオ再生を停止できません。ページを更新する必要があるかもしれません。', + com_nav_buffer_append_error: + 'オーディオストリーミングに問題が発生しました。再生が中断される可能性があります。', + com_nav_speech_cancel_error: + 'オーディオ再生を停止できません。ページを更新する必要があるかもしれません。', com_nav_voices_fetch_error: '音声オプションを取得できませんでした。インターネット接続を確認してください。', com_nav_engine: 'エンジン', @@ -753,8 +768,7 @@ export default { com_nav_commands: 'Commands', com_nav_commands_tab: 'コマンド設定', com_nav_at_command: '@-Command', - com_nav_at_command_description: - 'コマンド"@"でエンドポイント、モデル、プリセットを切り替える', + com_nav_at_command_description: 'コマンド"@"でエンドポイント、モデル、プリセットを切り替える', com_nav_plus_command: '+-Command', com_nav_plus_command_description: 'コマンド"+"で複数応答設定を追加する', com_nav_slash_command: '/-Command', @@ -894,7 +908,7 @@ export const comparisons = { english: 'Search assistants by name', translated: 'Assistantの名前で検索', }, - com_assistants_tools: { + com_ui_tools: { english: 'Tools', translated: 'Tools', }, diff --git a/client/src/localization/languages/Ko.ts b/client/src/localization/languages/Ko.ts index 7b2933ec9f..5a831d6e00 100644 --- a/client/src/localization/languages/Ko.ts +++ b/client/src/localization/languages/Ko.ts @@ -361,7 +361,7 @@ export default { com_assistants_code_interpreter_files: '코드 인터프리터에서만 다음 파일을 사용할 수 있습니다:', com_assistants_retrieval: '검색', com_assistants_search_name: '이름으로 도우미 검색', - com_assistants_tools: '도구', + com_ui_tools: '도구', com_assistants_actions: '작업', com_assistants_add_tools: '도구 추가', com_assistants_add_actions: '작업 추가', @@ -1910,7 +1910,7 @@ export const comparisons = { english: 'Search assistants by name', translated: '이름으로 도우미 검색', }, - com_assistants_tools: { + com_ui_tools: { english: 'Tools', translated: '도구', }, diff --git a/client/src/localization/languages/Ru.ts b/client/src/localization/languages/Ru.ts index e11dec0e6c..c41496d3d0 100644 --- a/client/src/localization/languages/Ru.ts +++ b/client/src/localization/languages/Ru.ts @@ -456,7 +456,7 @@ export default { com_assistants_capabilities: 'Возможности', com_assistants_image_vision: 'Анализ изображений', com_assistants_search_name: 'Поиск ассистентов по имени', - com_assistants_tools: 'Инструменты', + com_ui_tools: 'Инструменты', com_assistants_actions: 'Действия', com_assistants_add_tools: 'Добавить инструменты', com_assistants_add_actions: 'Добавить действия', @@ -2243,7 +2243,7 @@ export const comparisons = { english: 'Search assistants by name', translated: 'Поиск ассистентов по имени', }, - com_assistants_tools: { + com_ui_tools: { english: 'Tools', translated: 'Инструменты', }, diff --git a/client/src/localization/languages/Tr.ts b/client/src/localization/languages/Tr.ts index df19e0bdee..289d2c41df 100644 --- a/client/src/localization/languages/Tr.ts +++ b/client/src/localization/languages/Tr.ts @@ -35,7 +35,7 @@ export default { 'Aşağıdaki dosyalar yalnızca Kod Yorumlayıcı için kullanılabilir:', com_assistants_retrieval: 'Geri Getirme', com_assistants_search_name: 'Asistan adında ara', - com_assistants_tools: 'Araçlar', + com_ui_tools: 'Araçlar', com_assistants_actions: 'Eylemler', com_assistants_add_tools: 'Araçları Ekle', com_assistants_add_actions: 'Eylem Ekle', diff --git a/client/src/localization/languages/Zh.ts b/client/src/localization/languages/Zh.ts index db72f101c9..d09236943f 100644 --- a/client/src/localization/languages/Zh.ts +++ b/client/src/localization/languages/Zh.ts @@ -6,8 +6,7 @@ export default { com_nav_convo_menu_options: '对话菜单选项', com_ui_artifacts: 'Artifacts', com_ui_artifacts_toggle: '切换至 Artifacts UI', - com_nav_info_code_artifacts: - '启用在对话旁显示的实验性代码工件', + com_nav_info_code_artifacts: '启用在对话旁显示的实验性代码工件', com_ui_include_shadcnui: '包含 shadcn/ui 组件指令', com_nav_info_include_shadcnui: '启用后,将包含使用 shadcn/ui 组件的指令。shadcn/ui 是一组使用 Radix UI 和 Tailwind CSS 构建的可重复使用的组件。注意:这些指令较长,仅在您需要向 LLM 提供正确的导入和组件信息时才应启用。有关这些组件的更多信息,请访问:https://ui.shadcn.com/', @@ -23,8 +22,7 @@ export default { com_error_no_user_key: '没有找到密钥。请提供密钥后重试。', com_error_no_base_url: '未找到基础 URL,请提供一个后重试。', com_error_invalid_user_key: '提供的密钥无效。请提供有效的密钥后重试。', - com_error_expired_user_key: - '您提供的 {0} 密钥已于 {1} 过期。请提供新的密钥并重试。', + com_error_expired_user_key: '您提供的 {0} 密钥已于 {1} 过期。请提供新的密钥并重试。', com_error_input_length: '最新消息的令牌数过长,超出了令牌限制(分别为 {0})。请缩短您的消息、调整对话参数中的最大上下文大小,或分叉对话以继续。', com_files_no_results: '无结果。', @@ -44,8 +42,7 @@ export default { com_assistants_code_interpreter_info: '代码解释器使助手能够编写和运行代码。该工具可以处理具有多种数据和格式的文件,并生成图表等文件。', com_assistants_knowledge: '知识', - com_assistants_knowledge_info: - '如果您在 “知识” 中上传文件,与助手的对话可能包括文件内容。', + com_assistants_knowledge_info: '如果您在 “知识” 中上传文件,与助手的对话可能包括文件内容。', com_assistants_knowledge_disabled: '必须创建助手,且启用并保存代码解释器或检索,才能将文件作为知识上传。', com_assistants_image_vision: '识图', @@ -53,12 +50,11 @@ export default { com_assistants_code_interpreter_files: '以下文件仅适用于代码解释器:', com_assistants_retrieval: '检索', com_assistants_search_name: '根据名称搜索助手', - com_assistants_tools: '工具', + com_ui_tools: '工具', com_assistants_actions: '操作', com_assistants_add_tools: '添加工具', com_assistants_add_actions: '添加操作', - com_assistants_non_retrieval_model: - '此模型未启用文件搜索功能。请选择其他模型。', + com_assistants_non_retrieval_model: '此模型未启用文件搜索功能。请选择其他模型。', com_assistants_available_actions: '可用操作', com_assistants_running_action: '正在运行操作', com_assistants_completed_action: '与 {0} 聊天', @@ -109,8 +105,7 @@ export default { com_ui_attach_error_openai: '无法将助手文件附加到其他渠道', com_ui_attach_warn_endpoint: '不兼容的工具可能会忽略非助手文件', com_ui_attach_error_size: '超出渠道规定的文件大小:', - com_ui_attach_error: - '无法附加文件,请创建或选择一个对话,或尝试刷新页面。', + com_ui_attach_error: '无法附加文件,请创建或选择一个对话,或尝试刷新页面。', com_ui_examples: '示例', com_ui_new_chat: '创建新对话', com_ui_happy_birthday: '这是我的第一个生日!', @@ -219,8 +214,7 @@ export default { '此选项会分叉所有导向目标消息的消息分支,包括其相邻消息;换句话说,无论是否可见或在同一路径上,所有消息分支都会被包含在内。', com_ui_fork_info_start: '如果勾选,则根据上述选择的行为,从此消息开始到对话中最新的消息将被分叉。', - com_ui_fork_info_remember: - '选中此项可记住您的选择,以便下次分叉对话时更快捷地使用您偏好的选项。', + com_ui_fork_info_remember: '选中此项可记住您的选择,以便下次分叉对话时更快捷地使用您偏好的选项。', com_ui_fork_success: '对话分叉成功', com_ui_fork_processing: '正在分叉对话...', com_ui_fork_error: '分叉对话时出现错误', @@ -229,8 +223,7 @@ export default { com_ui_fork_remember: '记住', com_ui_fork_split_target_setting: '默认以目标消息开始分叉', com_ui_fork_split_target: '在此分叉', - com_ui_fork_remember_checked: - '您的选择将在使用后被记住。您可以随时在设置中更改。', + com_ui_fork_remember_checked: '您的选择将在使用后被记住。您可以随时在设置中更改。', com_ui_fork_all_target: '包含所有目标', com_ui_fork_branches: '包含相关分支', com_ui_fork_visible: '仅可见消息', @@ -304,12 +297,9 @@ export default { com_ui_share_retrieve_error: '检索共享链接时出现错误', com_ui_share_delete_error: '删除共享链接时出现错误', com_ui_share_create_message: '您的姓名以及您在共享后添加的任何消息将保持私密。', - com_ui_share_created_message: - '已创建到您聊天的共享链接。可以通过设置随时管理以前共享的聊天。', - com_ui_share_update_message: - '您的姓名、自定义指令以及您在共享后添加的任何消息将保持私密。', - com_ui_share_updated_message: - '已更新到您聊天的共享链接。可以通过设置随时管理以前共享的聊天。', + com_ui_share_created_message: '已创建到您聊天的共享链接。可以通过设置随时管理以前共享的聊天。', + com_ui_share_update_message: '您的姓名、自定义指令以及您在共享后添加的任何消息将保持私密。', + com_ui_share_updated_message: '已更新到您聊天的共享链接。可以通过设置随时管理以前共享的聊天。', com_ui_shared_link_not_found: '未找到共享链接', com_ui_delete_conversation: '删除对话?', com_ui_delete_confirm: '这将删除', @@ -319,8 +309,7 @@ export default { com_ui_delete_action_confirm: '您确定要删除此操作吗?', com_ui_delete_confirm_prompt_version_var: '这将删除选中版本的 “{0}”。如果没有其他版本,该提示词将被删除。', - com_ui_delete_assistant_confirm: - '您确定要删除此助手吗?该操作无法撤销。', + com_ui_delete_assistant_confirm: '您确定要删除此助手吗?该操作无法撤销。', com_ui_rename: '重命名', com_ui_archive: '归档', com_ui_archive_error: '归档对话失败', @@ -331,8 +320,7 @@ export default { com_ui_upload: '上传', com_ui_connect: '连接', com_ui_locked: '已锁定', - com_ui_upload_delay: - '上传 “{0}” 时比预期花了更长时间。文件正在进行检索索引,请稍候。', + com_ui_upload_delay: '上传 “{0}” 时比预期花了更长时间。文件正在进行检索索引,请稍候。', com_ui_privacy_policy: '隐私政策', com_ui_terms_of_service: '服务政策', com_ui_use_micrphone: '使用麦克风', @@ -355,16 +343,11 @@ export default { com_ui_bookmarks_filter: '筛选书签...', com_ui_no_bookmarks: '似乎您还没有书签。点击一个对话并添加一个新的书签', com_ui_no_conversation_id: '未找到对话 ID', - com_auth_error_login: - '无法登录,请确认提供的账户密码正确,并重新尝试。', - com_auth_error_login_rl: - '尝试登录次数过多,请稍后再试。', - com_auth_error_login_ban: - '根据我们的服务规则,您的帐号被暂时禁用。', - com_auth_error_login_server: - '内部服务器错误,请稍后再试。', - com_auth_error_login_unverified: - '您的账户尚未验证。请检查您的邮件以获取验证链接。', + com_auth_error_login: '无法登录,请确认提供的账户密码正确,并重新尝试。', + com_auth_error_login_rl: '尝试登录次数过多,请稍后再试。', + com_auth_error_login_ban: '根据我们的服务规则,您的帐号被暂时禁用。', + com_auth_error_login_server: '内部服务器错误,请稍后再试。', + com_auth_error_login_unverified: '您的账户尚未验证。请检查您的邮件以获取验证链接。', com_auth_no_account: '还没有账户?', com_auth_sign_up: '注册', com_auth_sign_in: '登录', @@ -387,8 +370,7 @@ export default { com_auth_password_not_match: '密码不一致', com_auth_continue: '继续', com_auth_create_account: '创建账户', - com_auth_error_create: - '注册账户过程中出现错误,请重试。', + com_auth_error_create: '注册账户过程中出现错误,请重试。', com_auth_full_name: '姓名', com_auth_name_required: '姓名为必填项', com_auth_name_min_length: '姓名至少 3 个字符', @@ -408,8 +390,7 @@ export default { com_auth_reset_password_link_sent: '密码重置链接已发送', com_auth_reset_password_if_email_exists: '如果存在使用该邮件地址的帐户,则会发送一封包含密码重置说明的邮件。请务必检查您的垃圾邮件文件夹。', - com_auth_reset_password_email_sent: - '如果用户已注册,将会发送一封邮件到收件箱。', + com_auth_reset_password_email_sent: '如果用户已注册,将会发送一封邮件到收件箱。', com_auth_reset_password_success: '密码重置成功', com_auth_login_with_new_password: '现在你可以使用你的新密码登录。', com_auth_error_invalid_reset_token: '重置密码的密钥已失效。', @@ -482,8 +463,7 @@ export default { 输入词元和生成词元的总长度受模型上下文长度的限制。如果该数值超过最大上下文词元数,您可能会遇到错误。`, com_endpoint_openai_temp: '值越高表示输出越随机,值越低表示输出越确定。建议不要同时改变此值和 Top P。', - com_endpoint_openai_max: - '最大生成词元数。输入词元长度由模型的上下文长度决定。', + com_endpoint_openai_max: '最大生成词元数。输入词元长度由模型的上下文长度决定。', com_endpoint_openai_topp: '相较于随机性的另一个取样方法,称为核采样,模型选取输出词元中大于 top_p(概率密度在整个概率分布中的比例)的结果。比如 top_p=0.1 表示只有概率占比为前 10% 的词元才会被考虑作为输出。建议不要同时改变此值和随机性。', com_endpoint_openai_freq: @@ -498,8 +478,7 @@ export default { '发送给Vision的图像分辨率。 “Low” 更便宜且更快,“High” 更详细但更昂贵,“Auto” 将基于图像分辨率自动在两者之间进行选择。', com_endpoint_openai_stop: '最多 4 个序列,API 将停止生成更多词元。', com_endpoint_openai_custom_name_placeholder: '为 AI 设置一个名称', - com_endpoint_openai_prompt_prefix_placeholder: - '在系统消息中添加自定义指令,默认为空', + com_endpoint_openai_prompt_prefix_placeholder: '在系统消息中添加自定义指令,默认为空', com_endpoint_anthropic_temp: '值介于 0 到 1 之间。对于分析性/选择性任务,值应更接近 0;对于创造性和生成性任务,值应更接近 1。我们建议更改该参数或 Top P,但不要同时更改这两个参数。', com_endpoint_anthropic_topp: @@ -556,8 +535,7 @@ export default { com_endpoint_use_active_assistant: '使用激活的助手', com_endpoint_assistant_model: '助手模型', com_endpoint_save_as_preset: '保存为预设', - com_endpoint_presets_clear_warning: - '确定要清除所有预设吗?此操作不可逆。', + com_endpoint_presets_clear_warning: '确定要清除所有预设吗?此操作不可逆。', com_endpoint_not_implemented: '未实现功能', com_endpoint_no_presets: '暂无预设,使用设置按钮创建一个', com_endpoint_not_available: '无可用渠道', @@ -567,8 +545,7 @@ export default { com_endpoint_agent_model: '代理模型(推荐: GPT-3.5)', com_endpoint_completion_model: '补全模型(推荐: GPT-4)', com_endpoint_func_hover: '将插件作为 OpenAI 函数使用', - com_endpoint_skip_hover: - '跳过补全步骤,检查最终答案和生成步骤', + com_endpoint_skip_hover: '跳过补全步骤,检查最终答案和生成步骤', com_endpoint_config_key: '设置 API 密钥', com_endpoint_assistant_placeholder: '请从右侧面板中选择助手', com_endpoint_config_placeholder: '在顶部菜单设置 API 密钥。', @@ -648,8 +625,7 @@ export default { com_show_examples: '显示示例', com_nav_plugin_search: '搜索插件', com_nav_tool_search: '搜索工具', - com_nav_plugin_auth_error: - '尝试验证此插件时出现错误。请重试。', + com_nav_plugin_auth_error: '尝试验证此插件时出现错误。请重试。', com_nav_export_filename: '文件名', com_nav_export_filename_placeholder: '设置文件名', com_nav_export_type: '类型', @@ -694,8 +670,7 @@ export default { com_nav_archive_name: '名称', com_nav_archive_created_at: '归档时间', com_nav_clear_conversation: '清除对话', - com_nav_clear_conversation_confirm_message: - '您确定要删除所有对话吗?该操作无法撤销。', + com_nav_clear_conversation_confirm_message: '您确定要删除所有对话吗?该操作无法撤销。', com_nav_help_faq: '帮助', com_nav_settings: '设置', com_nav_search_placeholder: '搜索消息', @@ -716,15 +691,12 @@ export default { com_nav_audio_process_error: '处理音频时发生错误:{0}', com_nav_long_audio_warning: '较长的文本将需要更长时间来处理。', com_nav_tts_init_error: '初始化文本转语音失败:{0}', - com_nav_tts_unsupported_error: - '所选引擎的文本转语音在此浏览器中不受支持。', + com_nav_tts_unsupported_error: '所选引擎的文本转语音在此浏览器中不受支持。', com_nav_source_buffer_error: '设置音频播放时发生错误。请刷新页面。', - com_nav_media_source_init_error: - '无法准备音频播放器。请检查您的浏览器设置。', + com_nav_media_source_init_error: '无法准备音频播放器。请检查您的浏览器设置。', com_nav_buffer_append_error: '音频流处理出现问题。播放可能会被中断。', com_nav_speech_cancel_error: '无法停止音频播放。您可能需要刷新页面。', - com_nav_voices_fetch_error: - '无法获取语音选项。请检查您的网络连接。', + com_nav_voices_fetch_error: '无法获取语音选项。请检查您的网络连接。', com_nav_engine: '引擎', com_nav_browser: '浏览器', com_nav_edge: '边缘端', @@ -753,8 +725,7 @@ export default { com_nav_commands: '命令', com_nav_commands_tab: '命令设置', com_nav_at_command: '@-命令', - com_nav_at_command_description: - '切换至命令 “@” 以更改端点、模型、预设等', + com_nav_at_command_description: '切换至命令 “@” 以更改端点、模型、预设等', com_nav_plus_command: '+-命令', com_nav_plus_command_description: '切换至命令 “+” 以添加多响应设置', com_nav_slash_command: '/-命令', diff --git a/client/src/localization/languages/ZhTraditional.ts b/client/src/localization/languages/ZhTraditional.ts index 8049e920b6..8d843fb964 100644 --- a/client/src/localization/languages/ZhTraditional.ts +++ b/client/src/localization/languages/ZhTraditional.ts @@ -360,7 +360,7 @@ export default { com_assistants_code_interpreter_files: '以下檔案僅適用於程式碼解譯器:', com_assistants_retrieval: '檢索', com_assistants_search_name: '搜尋助理名稱', - com_assistants_tools: '工具', + com_ui_tools: '工具', com_assistants_actions: '操作', com_assistants_add_tools: '新增工具', com_assistants_add_actions: '新增操作', @@ -1933,7 +1933,7 @@ export const comparisons = { english: 'Search assistants by name', translated: '搜尋助理名稱', }, - com_assistants_tools: { + com_ui_tools: { english: 'Tools', translated: '工具', }, diff --git a/client/src/localization/prompts/instructions/Br.md b/client/src/localization/prompts/instructions/Br.md index bbc6fc7e96..5cb628a784 100644 --- a/client/src/localization/prompts/instructions/Br.md +++ b/client/src/localization/prompts/instructions/Br.md @@ -71,7 +71,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it - **english**: Search assistants by name - **translated**: Pesquisar assistentes por nome -- **com_assistants_tools**: +- **com_ui_tools**: - **english**: Tools - **translated**: Ferramentas diff --git a/client/src/localization/prompts/instructions/De.md b/client/src/localization/prompts/instructions/De.md index a74515019e..ae21098b7a 100644 --- a/client/src/localization/prompts/instructions/De.md +++ b/client/src/localization/prompts/instructions/De.md @@ -75,7 +75,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it - **english**: Search assistants by name - **translated**: Assistenten nach Namen suchen -- **com_assistants_tools**: +- **com_ui_tools**: - **english**: Tools - **translated**: Werkzeuge diff --git a/client/src/localization/prompts/instructions/Es.md b/client/src/localization/prompts/instructions/Es.md index 6b7790b09a..747f8662c2 100644 --- a/client/src/localization/prompts/instructions/Es.md +++ b/client/src/localization/prompts/instructions/Es.md @@ -71,7 +71,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it - **english**: Search assistants by name - **translated**: Buscar asistentes por nombre -- **com_assistants_tools**: +- **com_ui_tools**: - **english**: Tools - **translated**: Herramientas diff --git a/client/src/localization/prompts/instructions/It.md b/client/src/localization/prompts/instructions/It.md index a7838e2045..2ce33f1c43 100644 --- a/client/src/localization/prompts/instructions/It.md +++ b/client/src/localization/prompts/instructions/It.md @@ -95,7 +95,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it - **english**: Search assistants by name - **translated**: Cerca assistenti per nome -- **com_assistants_tools**: +- **com_ui_tools**: - **english**: Tools - **translated**: Strumenti diff --git a/client/src/localization/prompts/instructions/Jp.md b/client/src/localization/prompts/instructions/Jp.md index 07db714911..6e725f0b89 100644 --- a/client/src/localization/prompts/instructions/Jp.md +++ b/client/src/localization/prompts/instructions/Jp.md @@ -91,7 +91,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it - **english**: Search assistants by name - **translated**: Assistantの名前で検索 -- **com_assistants_tools**: +- **com_ui_tools**: - **english**: Tools - **translated**: Tools diff --git a/client/src/localization/prompts/instructions/Zh.md b/client/src/localization/prompts/instructions/Zh.md index c9cb41a5cd..739a600e72 100644 --- a/client/src/localization/prompts/instructions/Zh.md +++ b/client/src/localization/prompts/instructions/Zh.md @@ -71,7 +71,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it - **english**: Search assistants by name - **translated**: 按名称搜索助手 -- **com_assistants_tools**: +- **com_ui_tools**: - **english**: Tools - **translated**: 工具 diff --git a/client/src/store/conversation.ts b/client/src/store/conversation.ts index ee12d71496..e6d27daac7 100644 --- a/client/src/store/conversation.ts +++ b/client/src/store/conversation.ts @@ -1,5 +1,5 @@ import { atom, selector, atomFamily } from 'recoil'; -import { TConversation, TMessagesAtom, TMessage } from 'librechat-data-provider'; +import { TConversation, TMessagesAtom, TMessage, TAttachment } from 'librechat-data-provider'; import { buildTree } from '~/utils'; const conversation = atom({ @@ -22,6 +22,11 @@ const messagesTree = selector({ }, }); +const messageAttachmentsMap = atom>({ + key: 'messageAttachmentsMap', + default: {}, +}); + const latestMessage = atom({ key: 'latestMessage', default: null, @@ -37,5 +42,6 @@ export default { conversation, messagesTree, latestMessage, + messageAttachmentsMap, messagesSiblingIdxFamily, }; diff --git a/client/src/store/settings.ts b/client/src/store/settings.ts index 0316336bd3..b4a8360b28 100644 --- a/client/src/store/settings.ts +++ b/client/src/store/settings.ts @@ -31,7 +31,7 @@ const localStorageAtoms = { // Messages settings enterToSend: atomWithLocalStorage('enterToSend', true), chatDirection: atomWithLocalStorage('chatDirection', 'LTR'), - showCode: atomWithLocalStorage('showCode', false), + showCode: atomWithLocalStorage(LocalStorageKeys.SHOW_ANALYSIS_CODE, true), saveDrafts: atomWithLocalStorage('saveDrafts', true), forkSetting: atomWithLocalStorage('forkSetting', ''), splitAtTarget: atomWithLocalStorage('splitAtTarget', false), diff --git a/client/src/utils/endpoints.ts b/client/src/utils/endpoints.ts index 06f822b116..30b2676837 100644 --- a/client/src/utils/endpoints.ts +++ b/client/src/utils/endpoints.ts @@ -3,19 +3,14 @@ import { defaultEndpoints, modularEndpoints, LocalStorageKeys, + isAgentsEndpoint, isAssistantsEndpoint, } from 'librechat-data-provider'; -import type { - TConfig, - TPreset, - TModelSpec, - TConversation, - TEndpointsConfig, -} from 'librechat-data-provider'; +import type * as t from 'librechat-data-provider'; import type { LocalizeFunction } from '~/common'; export const getAssistantName = ({ - name, + name = '', localize, }: { name?: string; @@ -28,7 +23,7 @@ export const getAssistantName = ({ } }; -export const getEndpointsFilter = (endpointsConfig: TEndpointsConfig) => { +export const getEndpointsFilter = (endpointsConfig: t.TEndpointsConfig) => { const filter: Record = {}; if (!endpointsConfig) { return filter; @@ -41,7 +36,7 @@ export const getEndpointsFilter = (endpointsConfig: TEndpointsConfig) => { export const getAvailableEndpoints = ( filter: Record, - endpointsConfig: TEndpointsConfig, + endpointsConfig: t.TEndpointsConfig, ) => { const defaultSet = new Set(defaultEndpoints); const availableEndpoints: EModelEndpoint[] = []; @@ -61,11 +56,11 @@ export const getAvailableEndpoints = ( }; /** Get the specified field from the endpoint config */ -export function getEndpointField( - endpointsConfig: TEndpointsConfig | undefined, +export function getEndpointField( + endpointsConfig: t.TEndpointsConfig | undefined, endpoint: EModelEndpoint | string | null | undefined, property: K, -): TConfig[K] | undefined { +): t.TConfig[K] | undefined { if (!endpointsConfig || endpoint === null || endpoint === undefined) { return undefined; } @@ -78,7 +73,7 @@ export function getEndpointField( return config[property]; } -export function mapEndpoints(endpointsConfig: TEndpointsConfig) { +export function mapEndpoints(endpointsConfig: t.TEndpointsConfig) { const filter = getEndpointsFilter(endpointsConfig); return getAvailableEndpoints(filter, endpointsConfig).sort( (a, b) => (endpointsConfig?.[a]?.order ?? 0) - (endpointsConfig?.[b]?.order ?? 0), @@ -113,14 +108,14 @@ export function updateLastSelectedModel({ } interface ConversationInitParams { - conversation: TConversation | null; - newEndpoint: EModelEndpoint | string; - endpointsConfig: TEndpointsConfig; + conversation: t.TConversation | null; + newEndpoint: EModelEndpoint | string | null; + endpointsConfig: t.TEndpointsConfig; modularChat?: boolean; } interface InitiatedTemplateResult { - template: Partial; + template: Partial; shouldSwitch: boolean; isExistingConversation: boolean; isCurrentModular: boolean; @@ -130,10 +125,10 @@ interface InitiatedTemplateResult { /** Get the conditional logic for switching conversations */ export function getConvoSwitchLogic(params: ConversationInitParams): InitiatedTemplateResult { - const { conversation, newEndpoint, endpointsConfig, modularChat } = params; + const { conversation, newEndpoint, endpointsConfig, modularChat = false } = params; const currentEndpoint = conversation?.endpoint; - const template: Partial = { + const template: Partial = { ...conversation, endpoint: newEndpoint, conversationId: 'new', @@ -144,7 +139,7 @@ export function getConvoSwitchLogic(params: ConversationInitParams): InitiatedTe isAssistantsEndpoint(currentEndpoint) && currentEndpoint === newEndpoint; - const conversationId = conversation?.conversationId; + const conversationId = conversation?.conversationId ?? ''; const isExistingConversation = !!(conversationId && conversationId !== 'new'); const currentEndpointType = @@ -178,7 +173,7 @@ export function getConvoSwitchLogic(params: ConversationInitParams): InitiatedTe * * First, the admin defined default, then last selected spec, followed by first spec */ -export function getDefaultModelSpec(modelSpecs?: TModelSpec[]) { +export function getDefaultModelSpec(modelSpecs?: t.TModelSpec[]) { const defaultSpec = modelSpecs?.find((spec) => spec.default); const lastSelectedSpecName = localStorage.getItem(LocalStorageKeys.LAST_SPEC); const lastSelectedSpec = modelSpecs?.find((spec) => spec.name === lastSelectedSpecName); @@ -189,7 +184,7 @@ export function getDefaultModelSpec(modelSpecs?: TModelSpec[]) { * * First, the admin defined default, then last selected spec, followed by first spec */ -export function getModelSpecIconURL(modelSpec: TModelSpec) { +export function getModelSpecIconURL(modelSpec: t.TModelSpec) { return modelSpec.iconURL ?? modelSpec.preset.iconURL ?? modelSpec.preset.endpoint ?? ''; } @@ -202,7 +197,7 @@ export function getIconEndpoint({ iconURL, endpoint, }: { - endpointsConfig: TEndpointsConfig | undefined; + endpointsConfig: t.TEndpointsConfig | undefined; iconURL: string | undefined; endpoint: string | null | undefined; }) { @@ -217,14 +212,44 @@ export function getIconKey({ endpointIconURL: iconURL, }: { endpoint?: string | null; - endpointsConfig?: TEndpointsConfig | undefined; + endpointsConfig?: t.TEndpointsConfig | undefined; endpointType?: string | null; endpointIconURL?: string; }) { - const endpointType = _eType ?? getEndpointField(endpointsConfig, endpoint, 'type'); - const endpointIconURL = iconURL ?? getEndpointField(endpointsConfig, endpoint, 'iconURL'); - if (endpointIconURL && EModelEndpoint[endpointIconURL]) { + const endpointType = _eType ?? getEndpointField(endpointsConfig, endpoint, 'type') ?? ''; + const endpointIconURL = iconURL ?? getEndpointField(endpointsConfig, endpoint, 'iconURL') ?? ''; + if (endpointIconURL && EModelEndpoint[endpointIconURL] != null) { return endpointIconURL; } return endpointType ? 'unknown' : endpoint ?? 'unknown'; } + +export const getEntity = ({ + endpoint, + assistant_id, + agent_id, + agentsMap, + assistantMap, +}: { + endpoint: EModelEndpoint | string | null | undefined; + assistant_id: string | undefined; + agent_id: string | undefined; + agentsMap: t.TAgentsMap | undefined; + assistantMap: t.TAssistantsMap | undefined; +}): { + entity: t.Agent | t.Assistant | undefined | null; + isAgent: boolean; + isAssistant: boolean; +} => { + const isAgent = isAgentsEndpoint(endpoint); + const isAssistant = isAssistantsEndpoint(endpoint); + + if (isAgent) { + const agent = agentsMap?.[agent_id ?? '']; + return { entity: agent, isAgent, isAssistant }; + } else if (isAssistant) { + const assistant = assistantMap?.[endpoint ?? '']?.[assistant_id ?? '']; + return { entity: assistant, isAgent, isAssistant }; + } + return { entity: null, isAgent, isAssistant }; +}; diff --git a/client/src/utils/forms.tsx b/client/src/utils/forms.tsx index 2447a234dd..04e52c9893 100644 --- a/client/src/utils/forms.tsx +++ b/client/src/utils/forms.tsx @@ -1,7 +1,12 @@ import { EarthIcon } from 'lucide-react'; -import { alternateName } from 'librechat-data-provider'; +import { + alternateName, + EModelEndpoint, + FileSources, + EToolResources, +} from 'librechat-data-provider'; import type { Agent, TFile } from 'librechat-data-provider'; -import type { DropdownValueSetter, TAgentOption } from '~/common'; +import type { DropdownValueSetter, TAgentOption, ExtendedFile } from '~/common'; /** * Creates a Dropdown value setter that always passes a string value, @@ -37,9 +42,6 @@ export const createProviderOption = (provider: string) => ({ value: provider, }); -type FileTuple = [string, Partial]; -type FileList = Array; - export const processAgentOption = ({ agent: _agent, fileMap, @@ -56,20 +58,49 @@ export const processAgentOption = ({ label: _agent?.name ?? '', value: _agent?.id ?? '', icon: isGlobal ? : null, - // files: _agent?.file_ids ? ([] as FileList) : undefined, - // code_files: _agent?.tool_resources?.code_interpreter?.file_ids - // ? ([] as FileList) - // : undefined, + knowledge_files: _agent?.tool_resources?.file_search?.file_ids + ? ([] as Array<[string, ExtendedFile]>) + : undefined, + code_files: _agent?.tool_resources?.execute_code?.file_ids + ? ([] as Array<[string, ExtendedFile]>) + : undefined, }; if (!fileMap) { return agent; } - const handleFile = (file_id: string, list?: FileList) => { + const handleFile = ({ + file_id, + tool_resource, + list, + }: { + file_id: string; + tool_resource: EToolResources; + list?: Array<[string, ExtendedFile]>; + }) => { const file = fileMap[file_id]; + const source = + tool_resource === EToolResources.file_search + ? FileSources.vectordb + : file?.source ?? FileSources.local; + if (file) { - list?.push([file_id, file]); + list?.push([ + file_id, + { + file_id: file.file_id, + type: file.type, + filepath: file.filepath, + filename: file.filename, + width: file.width, + height: file.height, + size: file.bytes, + preview: file.filepath, + progress: 1, + source, + }, + ]); } else { list?.push([ file_id, @@ -77,22 +108,28 @@ export const processAgentOption = ({ file_id, type: '', filename: '', - bytes: 1, - // progress: 1, - // TODO: file handling - // filepath: endpoint, + size: 1, + progress: 1, + filepath: EModelEndpoint.agents, + source, }, ]); } }; - if (agent.files && _agent?.file_ids) { - _agent.file_ids.forEach((file_id) => handleFile(file_id, agent.files)); + if (agent.knowledge_files && _agent?.tool_resources?.file_search?.file_ids) { + _agent.tool_resources.file_search.file_ids.forEach((file_id) => + handleFile({ + file_id, + list: agent.knowledge_files, + tool_resource: EToolResources.file_search, + }), + ); } - if (agent.code_files && _agent?.tool_resources?.code_interpreter?.file_ids) { - _agent.tool_resources.code_interpreter.file_ids.forEach((file_id) => - handleFile(file_id, agent.code_files), + if (agent.code_files && _agent?.tool_resources?.execute_code?.file_ids) { + _agent.tool_resources.execute_code.file_ids.forEach((file_id) => + handleFile({ file_id, list: agent.code_files, tool_resource: EToolResources.execute_code }), ); } diff --git a/client/src/utils/map.ts b/client/src/utils/map.ts index c490053af8..45fc5f0645 100644 --- a/client/src/utils/map.ts +++ b/client/src/utils/map.ts @@ -1,6 +1,29 @@ -import type { TFile, Assistant, Agent, TPlugin } from 'librechat-data-provider'; +import type { TFile, TAttachment, Assistant, Agent, TPlugin } from 'librechat-data-provider'; import type { TPluginMap } from '~/common'; +/** Maps Attachments by `toolCallId` for quick lookup */ +export function mapAttachments(attachments: Array) { + const attachmentMap: Record = {}; + + for (const attachment of attachments) { + if (attachment === null || attachment === undefined) { + continue; + } + const key = attachment.toolCallId ?? ''; + if (key.length === 0) { + continue; + } + + if (!attachmentMap[key]) { + attachmentMap[key] = []; + } + + attachmentMap[key].push(attachment); + } + + return attachmentMap; +} + /** Maps Files by `file_id` for quick lookup */ export function mapFiles(files: TFile[]) { const fileMap = {} as Record; diff --git a/package-lock.json b/package-lock.json index 9172e47c01..6fa20f5aa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,7 @@ "@langchain/core": "^0.2.18", "@langchain/google-genai": "^0.0.11", "@langchain/google-vertexai": "^0.0.17", - "@librechat/agents": "^1.5.2", + "@librechat/agents": "^1.6.9", "axios": "^1.7.7", "bcryptjs": "^2.4.3", "cheerio": "^1.0.0-rc.12", @@ -1030,16 +1030,15 @@ } }, "api/node_modules/@langchain/core": { - "version": "0.2.18", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.18.tgz", - "integrity": "sha512-ru542BwNcsnDfjTeDbIkFIchwa54ctHZR+kVrC8U9NPS9/36iM8p8ruprOV7Zccj/oxtLE5UpEhV+9MZhVcFlA==", + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.34.tgz", + "integrity": "sha512-Hkveq1UcOjUj1DVn5erbqElyRj1t04NORSuSIZAJCtPO7EDkIqomjAarJ5+I5NUpQeIONgbOdnY9TkJ6cKUSVA==", "dependencies": { "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": "~0.1.39", - "ml-distance": "^4.0.0", + "langsmith": "^0.1.56-rc.1", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", @@ -1051,40 +1050,6 @@ "node": ">=18" } }, - "api/node_modules/@langchain/core/node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==" - }, - "api/node_modules/@langchain/core/node_modules/langsmith": { - "version": "0.1.49", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.49.tgz", - "integrity": "sha512-PI9qzBalZYpN2gu1kf1IX/+unRRXTiZRhtTKYfWcPyQVPZ5exZVJfZy515+FUp1iXdgacr2tS2ZtNGUB2Vdejg==", - "dependencies": { - "@types/uuid": "^10.0.0", - "commander": "^10.0.1", - "p-queue": "^6.6.2", - "p-retry": "4", - "semver": "^7.6.3", - "uuid": "^10.0.0" - }, - "peerDependencies": { - "@langchain/core": "*", - "langchain": "*", - "openai": "*" - }, - "peerDependenciesMeta": { - "@langchain/core": { - "optional": true - }, - "langchain": { - "optional": true - }, - "openai": { - "optional": true - } - } - }, "api/node_modules/@langchain/core/node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", @@ -1105,6 +1070,11 @@ "undici-types": "~5.26.4" } }, + "api/node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==" + }, "api/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -1150,15 +1120,36 @@ } }, "api/node_modules/langsmith": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.14.tgz", - "integrity": "sha512-iEzQLLB7/0nRpAwNBAR7B7N64fyByg5UsNjSvLaCCkQ9AS68PSafjB8xQkyI8QXXrGjU1dEqDRoa8m4SUuRdUw==", + "version": "0.1.59", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.59.tgz", + "integrity": "sha512-dW+z6s538zBswFFP2w/xzvVef7y2+yNt6GkmRCeLtwfpbMaM4di7JboK3vmnZ+0/LjNb2ukiMmgsTNKu/Y43cg==", "dependencies": { - "@types/uuid": "^9.0.1", + "@types/uuid": "^10.0.0", "commander": "^10.0.1", "p-queue": "^6.6.2", "p-retry": "4", - "uuid": "^9.0.0" + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "openai": "*" + }, + "peerDependenciesMeta": { + "openai": { + "optional": true + } + } + }, + "api/node_modules/langsmith/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" } }, "api/node_modules/node-fetch": { @@ -1181,9 +1172,9 @@ } }, "api/node_modules/openai": { - "version": "4.47.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.47.1.tgz", - "integrity": "sha512-WWSxhC/69ZhYWxH/OBsLEirIjUcfpQ5+ihkXKp06hmeYXgBBIUCa9IptMzYx6NdkiOCsSGYCnTIsxaic3AjRCQ==", + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.63.0.tgz", + "integrity": "sha512-Y9V4KODbmrOpqiOmCDVnPfMxMqKLOx8Hwcdn/r8mePq4yv7FSXGnxCs8/jZKO7zCB/IVPWihpJXwJNAIOEiZ2g==", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -1191,11 +1182,18 @@ "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7", - "web-streams-polyfill": "^3.2.1" + "node-fetch": "^2.6.7" }, "bin": { "openai": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } } }, "api/node_modules/tr46": { @@ -8790,9 +8788,9 @@ } }, "node_modules/@langchain/anthropic": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-0.2.15.tgz", - "integrity": "sha512-b9zVryoqqzyKelCJoc/8R8EziGfUyaX9w13BNXGoZ+RwMU88XaM05xeqxcJFOezVTi0aH4yoEZ6Kw/CtsukI0A==", + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-0.2.18.tgz", + "integrity": "sha512-4ZDTxMwGKTPRAi2Supu/faBSmwPIm/ga5QlazyO78Mf/8QbDR2DcvM5394FAy+X/nRAfnMbyXteO5IRJm653gw==", "dependencies": { "@anthropic-ai/sdk": "^0.25.2", "@langchain/core": ">=0.2.21 <0.3.0", @@ -8819,15 +8817,15 @@ } }, "node_modules/@langchain/anthropic/node_modules/@langchain/core": { - "version": "0.2.31", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.31.tgz", - "integrity": "sha512-qGfeaACST7dvovgHItzuag9fEBGK7IjCE9vRuTu/y8/WYGJi28WPD/AwWxnu7YdW1vZSuIXO6ZA76t2G9B/oKg==", + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.34.tgz", + "integrity": "sha512-Hkveq1UcOjUj1DVn5erbqElyRj1t04NORSuSIZAJCtPO7EDkIqomjAarJ5+I5NUpQeIONgbOdnY9TkJ6cKUSVA==", "dependencies": { "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": "^0.1.43", + "langsmith": "^0.1.56-rc.1", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", @@ -8840,9 +8838,9 @@ } }, "node_modules/@langchain/anthropic/node_modules/@types/node": { - "version": "18.19.48", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.48.tgz", - "integrity": "sha512-7WevbG4ekUcRQSZzOwxWgi5dZmTak7FaxXDoW7xVxPBmKx1rTzfmRLkeCgJzcbBnOV2dkhAPc8cCeT6agocpjg==", + "version": "18.19.53", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.53.tgz", + "integrity": "sha512-GLxgUgHhDKO1Edw9Q0lvMbiO/IQXJwJlMaqxSGBXMpPy8uhkCs2iiPFaB2Q/gmobnFkckD3rqTBMVjXdwq+nKg==", "dependencies": { "undici-types": "~5.26.4" } @@ -8864,9 +8862,9 @@ } }, "node_modules/@langchain/anthropic/node_modules/langsmith": { - "version": "0.1.49", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.49.tgz", - "integrity": "sha512-PI9qzBalZYpN2gu1kf1IX/+unRRXTiZRhtTKYfWcPyQVPZ5exZVJfZy515+FUp1iXdgacr2tS2ZtNGUB2Vdejg==", + "version": "0.1.61", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.61.tgz", + "integrity": "sha512-XQE4KPScwPmdaT0mWDzhNxj9gvqXUR+C7urLA0QFi27XeoQdm17eYpudenn4wxC0gIyUJutQCyuYJpfwlT5JnQ==", "dependencies": { "@types/uuid": "^10.0.0", "commander": "^10.0.1", @@ -8876,17 +8874,9 @@ "uuid": "^10.0.0" }, "peerDependencies": { - "@langchain/core": "*", - "langchain": "*", "openai": "*" }, "peerDependenciesMeta": { - "@langchain/core": { - "optional": true - }, - "langchain": { - "optional": true - }, "openai": { "optional": true } @@ -8922,15 +8912,15 @@ } }, "node_modules/@langchain/aws/node_modules/@langchain/core": { - "version": "0.2.31", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.31.tgz", - "integrity": "sha512-qGfeaACST7dvovgHItzuag9fEBGK7IjCE9vRuTu/y8/WYGJi28WPD/AwWxnu7YdW1vZSuIXO6ZA76t2G9B/oKg==", + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.34.tgz", + "integrity": "sha512-Hkveq1UcOjUj1DVn5erbqElyRj1t04NORSuSIZAJCtPO7EDkIqomjAarJ5+I5NUpQeIONgbOdnY9TkJ6cKUSVA==", "dependencies": { "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": "^0.1.43", + "langsmith": "^0.1.56-rc.1", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", @@ -8959,9 +8949,9 @@ } }, "node_modules/@langchain/aws/node_modules/langsmith": { - "version": "0.1.49", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.49.tgz", - "integrity": "sha512-PI9qzBalZYpN2gu1kf1IX/+unRRXTiZRhtTKYfWcPyQVPZ5exZVJfZy515+FUp1iXdgacr2tS2ZtNGUB2Vdejg==", + "version": "0.1.61", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.61.tgz", + "integrity": "sha512-XQE4KPScwPmdaT0mWDzhNxj9gvqXUR+C7urLA0QFi27XeoQdm17eYpudenn4wxC0gIyUJutQCyuYJpfwlT5JnQ==", "dependencies": { "@types/uuid": "^10.0.0", "commander": "^10.0.1", @@ -8971,17 +8961,9 @@ "uuid": "^10.0.0" }, "peerDependencies": { - "@langchain/core": "*", - "langchain": "*", "openai": "*" }, "peerDependenciesMeta": { - "@langchain/core": { - "optional": true - }, - "langchain": { - "optional": true - }, "openai": { "optional": true } @@ -9607,15 +9589,15 @@ } }, "node_modules/@langchain/langgraph/node_modules/@langchain/core": { - "version": "0.2.31", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.31.tgz", - "integrity": "sha512-qGfeaACST7dvovgHItzuag9fEBGK7IjCE9vRuTu/y8/WYGJi28WPD/AwWxnu7YdW1vZSuIXO6ZA76t2G9B/oKg==", + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.34.tgz", + "integrity": "sha512-Hkveq1UcOjUj1DVn5erbqElyRj1t04NORSuSIZAJCtPO7EDkIqomjAarJ5+I5NUpQeIONgbOdnY9TkJ6cKUSVA==", "dependencies": { "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": "^0.1.43", + "langsmith": "^0.1.56-rc.1", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", @@ -9644,9 +9626,9 @@ } }, "node_modules/@langchain/langgraph/node_modules/langsmith": { - "version": "0.1.49", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.49.tgz", - "integrity": "sha512-PI9qzBalZYpN2gu1kf1IX/+unRRXTiZRhtTKYfWcPyQVPZ5exZVJfZy515+FUp1iXdgacr2tS2ZtNGUB2Vdejg==", + "version": "0.1.61", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.61.tgz", + "integrity": "sha512-XQE4KPScwPmdaT0mWDzhNxj9gvqXUR+C7urLA0QFi27XeoQdm17eYpudenn4wxC0gIyUJutQCyuYJpfwlT5JnQ==", "dependencies": { "@types/uuid": "^10.0.0", "commander": "^10.0.1", @@ -9656,17 +9638,9 @@ "uuid": "^10.0.0" }, "peerDependencies": { - "@langchain/core": "*", - "langchain": "*", "openai": "*" }, "peerDependenciesMeta": { - "@langchain/core": { - "optional": true - }, - "langchain": { - "optional": true - }, "openai": { "optional": true } @@ -9700,15 +9674,15 @@ } }, "node_modules/@langchain/mistralai/node_modules/@langchain/core": { - "version": "0.2.31", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.31.tgz", - "integrity": "sha512-qGfeaACST7dvovgHItzuag9fEBGK7IjCE9vRuTu/y8/WYGJi28WPD/AwWxnu7YdW1vZSuIXO6ZA76t2G9B/oKg==", + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.34.tgz", + "integrity": "sha512-Hkveq1UcOjUj1DVn5erbqElyRj1t04NORSuSIZAJCtPO7EDkIqomjAarJ5+I5NUpQeIONgbOdnY9TkJ6cKUSVA==", "dependencies": { "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": "^0.1.43", + "langsmith": "^0.1.56-rc.1", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", @@ -9737,9 +9711,9 @@ } }, "node_modules/@langchain/mistralai/node_modules/langsmith": { - "version": "0.1.49", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.49.tgz", - "integrity": "sha512-PI9qzBalZYpN2gu1kf1IX/+unRRXTiZRhtTKYfWcPyQVPZ5exZVJfZy515+FUp1iXdgacr2tS2ZtNGUB2Vdejg==", + "version": "0.1.61", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.61.tgz", + "integrity": "sha512-XQE4KPScwPmdaT0mWDzhNxj9gvqXUR+C7urLA0QFi27XeoQdm17eYpudenn4wxC0gIyUJutQCyuYJpfwlT5JnQ==", "dependencies": { "@types/uuid": "^10.0.0", "commander": "^10.0.1", @@ -9749,17 +9723,9 @@ "uuid": "^10.0.0" }, "peerDependencies": { - "@langchain/core": "*", - "langchain": "*", "openai": "*" }, "peerDependenciesMeta": { - "@langchain/core": { - "optional": true - }, - "langchain": { - "optional": true - }, "openai": { "optional": true } @@ -9831,15 +9797,15 @@ } }, "node_modules/@langchain/textsplitters/node_modules/@langchain/core": { - "version": "0.2.31", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.31.tgz", - "integrity": "sha512-qGfeaACST7dvovgHItzuag9fEBGK7IjCE9vRuTu/y8/WYGJi28WPD/AwWxnu7YdW1vZSuIXO6ZA76t2G9B/oKg==", + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.34.tgz", + "integrity": "sha512-Hkveq1UcOjUj1DVn5erbqElyRj1t04NORSuSIZAJCtPO7EDkIqomjAarJ5+I5NUpQeIONgbOdnY9TkJ6cKUSVA==", "dependencies": { "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": "^0.1.43", + "langsmith": "^0.1.56-rc.1", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", @@ -9868,9 +9834,9 @@ } }, "node_modules/@langchain/textsplitters/node_modules/langsmith": { - "version": "0.1.49", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.49.tgz", - "integrity": "sha512-PI9qzBalZYpN2gu1kf1IX/+unRRXTiZRhtTKYfWcPyQVPZ5exZVJfZy515+FUp1iXdgacr2tS2ZtNGUB2Vdejg==", + "version": "0.1.61", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.61.tgz", + "integrity": "sha512-XQE4KPScwPmdaT0mWDzhNxj9gvqXUR+C7urLA0QFi27XeoQdm17eYpudenn4wxC0gIyUJutQCyuYJpfwlT5JnQ==", "dependencies": { "@types/uuid": "^10.0.0", "commander": "^10.0.1", @@ -9880,17 +9846,9 @@ "uuid": "^10.0.0" }, "peerDependencies": { - "@langchain/core": "*", - "langchain": "*", "openai": "*" }, "peerDependenciesMeta": { - "@langchain/core": { - "optional": true - }, - "langchain": { - "optional": true - }, "openai": { "optional": true } @@ -9960,9 +9918,9 @@ } }, "node_modules/@librechat/agents": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-1.5.2.tgz", - "integrity": "sha512-mPBmzzidqTxsQ2nCNgPEqZnNzHKY6QvAr6mF+MTjsME/qQbqN+PjGUSQTCcvEKa3ClQK8yrZl8nNELFq9Ghv/w==", + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-1.6.9.tgz", + "integrity": "sha512-a2ef4cpC8BpegiIOJkC5rVqtp2lAfTEm/wWUfQ/wtEFPPyUBlnzoQRcO79V3Veju3DzkmMBJP5oUrzGZ+lKemg==", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-sdk/credential-provider-node": "^3.613.0", @@ -9987,9 +9945,9 @@ } }, "node_modules/@librechat/agents/node_modules/@langchain/community": { - "version": "0.2.31", - "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.2.31.tgz", - "integrity": "sha512-UayfsFyxHBLGnc9nODLupC46lvTMssYkfmFalMKqWAWQLWPEKOcDriy8Dg8+0MBME/y7SzqlCniEeEvml16hog==", + "version": "0.2.33", + "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.2.33.tgz", + "integrity": "sha512-YsytROnBYoPqtUcV2In+afyLACzxwFrYRn7EBKYL7XWl3XNwrT85U1+nLM5b+MOjXvg9YfJSjrs1Tlbfy4st8g==", "dependencies": { "@langchain/core": ">=0.2.21 <0.3.0", "@langchain/openai": ">=0.2.0 <0.3.0", @@ -10007,6 +9965,7 @@ "node": ">=18" }, "peerDependencies": { + "@arcjet/redact": "^v1.0.0-alpha.23", "@aws-crypto/sha256-js": "^5.0.0", "@aws-sdk/client-bedrock-agent-runtime": "^3.583.0", "@aws-sdk/client-bedrock-runtime": "^3.422.0", @@ -10036,7 +9995,7 @@ "@langchain/langgraph": "*", "@layerup/layerup-security": "^1.5.12", "@mendable/firecrawl-js": "^0.0.13", - "@mlc-ai/web-llm": "0.2.46", + "@mlc-ai/web-llm": "*", "@mozilla/readability": "*", "@neondatabase/serverless": "*", "@notionhq/client": "^2.2.10", @@ -10129,6 +10088,9 @@ "youtubei.js": "^9.1.0" }, "peerDependenciesMeta": { + "@arcjet/redact": { + "optional": true + }, "@aws-crypto/sha256-js": { "optional": true }, @@ -10492,15 +10454,15 @@ } }, "node_modules/@librechat/agents/node_modules/@langchain/core": { - "version": "0.2.31", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.31.tgz", - "integrity": "sha512-qGfeaACST7dvovgHItzuag9fEBGK7IjCE9vRuTu/y8/WYGJi28WPD/AwWxnu7YdW1vZSuIXO6ZA76t2G9B/oKg==", + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.34.tgz", + "integrity": "sha512-Hkveq1UcOjUj1DVn5erbqElyRj1t04NORSuSIZAJCtPO7EDkIqomjAarJ5+I5NUpQeIONgbOdnY9TkJ6cKUSVA==", "dependencies": { "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": "^0.1.43", + "langsmith": "^0.1.56-rc.1", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", @@ -10525,13 +10487,13 @@ } }, "node_modules/@librechat/agents/node_modules/@langchain/openai": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.2.8.tgz", - "integrity": "sha512-p5fxEAKuR8UV9jWIxkZ6AY/vAPSYxJI0Pf/UM4T3FKk/dn99G/mAEDLhfI4pBf7B8o8TudSVyBW2hRjZqlQu7g==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.2.11.tgz", + "integrity": "sha512-Pu8+WfJojCgSf0bAsXb4AjqvcDyAWyoEB1AoCRNACgEnBWZuitz3hLwCo9I+6hAbeg3QJ37g82yKcmvKAg1feg==", "dependencies": { "@langchain/core": ">=0.2.26 <0.3.0", "js-tiktoken": "^1.0.12", - "openai": "^4.55.0", + "openai": "^4.57.3", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.3" }, @@ -10540,9 +10502,9 @@ } }, "node_modules/@librechat/agents/node_modules/@types/node": { - "version": "18.19.48", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.48.tgz", - "integrity": "sha512-7WevbG4ekUcRQSZzOwxWgi5dZmTak7FaxXDoW7xVxPBmKx1rTzfmRLkeCgJzcbBnOV2dkhAPc8cCeT6agocpjg==", + "version": "18.19.53", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.53.tgz", + "integrity": "sha512-GLxgUgHhDKO1Edw9Q0lvMbiO/IQXJwJlMaqxSGBXMpPy8uhkCs2iiPFaB2Q/gmobnFkckD3rqTBMVjXdwq+nKg==", "dependencies": { "undici-types": "~5.26.4" } @@ -10576,9 +10538,9 @@ } }, "node_modules/@librechat/agents/node_modules/langchain": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.2.17.tgz", - "integrity": "sha512-wFn7wo+XGzqYrv3KJLmMZ1M6BHx12C3YUSASOa03rcDsBzRL5onxhKAC/g4xAIqlAHrJYgU6Jb/T/S6uJ6UdkQ==", + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.2.20.tgz", + "integrity": "sha512-tbels6Rr524iMM3VOQ4aTGnEOOjAA1BQuBR8u/8gJ2yT48lMtIQRAN32Y4KVjKK+hEWxHHlmLBrtgLpTphFjNA==", "dependencies": { "@langchain/core": ">=0.2.21 <0.3.0", "@langchain/openai": ">=0.1.0 <0.3.0", @@ -10587,7 +10549,7 @@ "js-tiktoken": "^1.0.12", "js-yaml": "^4.1.0", "jsonpointer": "^5.0.1", - "langsmith": "~0.1.40", + "langsmith": "^0.1.56-rc.1", "openapi-types": "^12.1.3", "p-retry": "4", "uuid": "^10.0.0", @@ -10611,7 +10573,6 @@ "@langchain/anthropic": "*", "@langchain/aws": "*", "@langchain/cohere": "*", - "@langchain/community": "*", "@langchain/google-genai": "*", "@langchain/google-vertexai": "*", "@langchain/groq": "*", @@ -10647,7 +10608,7 @@ "peggy": "^3.0.2", "playwright": "*", "puppeteer": "*", - "pyodide": "^0.24.1", + "pyodide": ">=0.24.1 <0.27.0", "redis": "*", "sonix-speech-recognition": "*", "srt-parser-2": "*", @@ -10695,9 +10656,6 @@ "@langchain/cohere": { "optional": true }, - "@langchain/community": { - "optional": true - }, "@langchain/google-genai": { "optional": true }, @@ -10839,9 +10797,9 @@ } }, "node_modules/@librechat/agents/node_modules/langsmith": { - "version": "0.1.49", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.49.tgz", - "integrity": "sha512-PI9qzBalZYpN2gu1kf1IX/+unRRXTiZRhtTKYfWcPyQVPZ5exZVJfZy515+FUp1iXdgacr2tS2ZtNGUB2Vdejg==", + "version": "0.1.61", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.61.tgz", + "integrity": "sha512-XQE4KPScwPmdaT0mWDzhNxj9gvqXUR+C7urLA0QFi27XeoQdm17eYpudenn4wxC0gIyUJutQCyuYJpfwlT5JnQ==", "dependencies": { "@types/uuid": "^10.0.0", "commander": "^10.0.1", @@ -10851,17 +10809,9 @@ "uuid": "^10.0.0" }, "peerDependencies": { - "@langchain/core": "*", - "langchain": "*", "openai": "*" }, "peerDependenciesMeta": { - "@langchain/core": { - "optional": true - }, - "langchain": { - "optional": true - }, "openai": { "optional": true } @@ -10878,19 +10828,17 @@ } }, "node_modules/@librechat/agents/node_modules/openai": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.57.0.tgz", - "integrity": "sha512-JnwBSIYqiZ3jYjB5f2in8hQ0PRA092c6m+/6dYB0MzK0BEbn+0dioxZsPLBm5idJbg9xzLNOiGVm2OSuhZ+BdQ==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.65.0.tgz", + "integrity": "sha512-LfA4KUBpH/8rA3vjCQ74LZtdK/8wx9W6Qxq8MHqEdImPsN1XPQ2ompIuJWkKS6kXt5Cs5i8Eb65IIo4M7U+yeQ==", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", - "@types/qs": "^6.9.7", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7", - "qs": "^6.10.3" + "node-fetch": "^2.6.7" }, "bin": { "openai": "bin/cli" @@ -15258,11 +15206,6 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, - "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" - }, "node_modules/@types/react": { "version": "18.2.53", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.53.tgz", @@ -36597,7 +36540,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.7.426", + "version": "0.7.427", "license": "ISC", "dependencies": { "@types/js-yaml": "^4.0.9", diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index 523d962f82..fa1924a4b4 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.7.426", + "version": "0.7.427", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index be0dfd3d8e..b95e0f138e 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -139,6 +139,13 @@ export enum Capabilities { tools = 'tools', } +export enum AgentCapabilities { + execute_code = 'execute_code', + file_search = 'file_search', + actions = 'actions', + tools = 'tools', +} + export const defaultAssistantsVersion = { [EModelEndpoint.assistants]: 2, [EModelEndpoint.azureAssistants]: 1, @@ -1121,6 +1128,8 @@ export enum LocalStorageKeys { LAST_PROMPT_CATEGORY = 'lastPromptCategory', /** Key for rendering User Messages as Markdown */ ENABLE_USER_MSG_MARKDOWN = 'enableUserMsgMarkdown', + /** Key for displaying analysis tool code input */ + SHOW_ANALYSIS_CODE = 'showAnalysisCode', } export enum ForkOptions { diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index 77114c6011..232136a0d7 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -482,13 +482,23 @@ export const getFileDownload = async (userId: string, file_id: string): Promise< }); }; -export const deleteFiles = async ( - files: f.BatchFile[], - assistant_id?: string, - tool_resource?: a.EToolResources, -): Promise => +export const getCodeOutputDownload = async (url: string): Promise => { + return request.getResponse(url, { + responseType: 'blob', + headers: { + Accept: 'application/octet-stream', + }, + }); +}; + +export const deleteFiles = async (payload: { + files: f.BatchFile[]; + agent_id?: string; + assistant_id?: string; + tool_resource?: a.EToolResources; +}): Promise => request.deleteWithOptions(endpoints.files(), { - data: { files, assistant_id, tool_resource }, + data: payload, }); /* Speech */ diff --git a/packages/data-provider/src/file-config.ts b/packages/data-provider/src/file-config.ts index f1c4ff9941..868e3f066a 100644 --- a/packages/data-provider/src/file-config.ts +++ b/packages/data-provider/src/file-config.ts @@ -168,6 +168,7 @@ export const fileConfig = { endpoints: { [EModelEndpoint.assistants]: assistantsFileConfig, [EModelEndpoint.azureAssistants]: assistantsFileConfig, + [EModelEndpoint.agents]: assistantsFileConfig, default: { fileLimit: 10, fileSizeLimit: defaultSizeLimit, diff --git a/packages/data-provider/src/generate.ts b/packages/data-provider/src/generate.ts index 972e5d2ac1..3751d65c90 100644 --- a/packages/data-provider/src/generate.ts +++ b/packages/data-provider/src/generate.ts @@ -85,7 +85,7 @@ export type DynamicSettingProps = Partial & { readonly?: boolean; settingKey: string; setOption: TSetOption; - conversation: TConversation | TPreset | null; + conversation: Partial | Partial | null; defaultValue?: number | boolean | string | string[]; className?: string; inputClassName?: string; diff --git a/packages/data-provider/src/roles.ts b/packages/data-provider/src/roles.ts index fca276b00e..1a0d53f3ce 100644 --- a/packages/data-provider/src/roles.ts +++ b/packages/data-provider/src/roles.ts @@ -43,6 +43,9 @@ export enum Permissions { SHARED_GLOBAL = 'SHARED_GLOBAL', USE = 'USE', CREATE = 'CREATE', + UPDATE = 'UPDATE', + READ = 'READ', + READ_AUTHOR = 'READ_AUTHOR', SHARE = 'SHARE', } diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 1e1a93cdba..1c4c24f92e 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -140,9 +140,9 @@ export const defaultAgentFormValues = { tools: [], provider: {}, projectIds: [], - code_interpreter: false, - image_vision: false, - retrieval: false, + isCollaborative: false, + [Tools.execute_code]: false, + [Tools.file_search]: false, }; export const ImageVisionTool: FunctionTool = { @@ -468,6 +468,13 @@ export const tMessageSchema = z.object({ iconURL: z.string().optional(), }); +export type TAttachmentMetadata = { messageId: string; toolCallId: string }; +export type TAttachment = + | (TFile & TAttachmentMetadata) + | (Pick & { + expiresAt: number; + } & TAttachmentMetadata); + export type TMessage = z.input & { children?: TMessage[]; plugin?: TResPlugin | null; @@ -476,6 +483,7 @@ export type TMessage = z.input & { files?: Partial[]; depth?: number; siblingIndex?: number; + attachments?: TAttachment[]; }; export const coerceNumber = z.union([z.number(), z.string()]).transform((val) => { diff --git a/packages/data-provider/src/types/assistants.ts b/packages/data-provider/src/types/assistants.ts index 0768a3d5b9..fde34fd13d 100644 --- a/packages/data-provider/src/types/assistants.ts +++ b/packages/data-provider/src/types/assistants.ts @@ -15,6 +15,7 @@ export type Metadata = { }; export enum Tools { + execute_code = 'execute_code', code_interpreter = 'code_interpreter', file_search = 'file_search', retrieval = 'retrieval', @@ -23,6 +24,7 @@ export enum Tools { export enum EToolResources { code_interpreter = 'code_interpreter', + execute_code = 'execute_code', file_search = 'file_search', } @@ -142,32 +144,61 @@ export type File = { /* Agent types */ +export type AgentParameterValue = number | null; + export type AgentModelParameters = { - temperature: number | null; - max_context_tokens: number | null; - max_output_tokens: number | null; - top_p: number | null; - frequency_penalty: number | null; - presence_penalty: number | null; + temperature: AgentParameterValue; + max_context_tokens: AgentParameterValue; + max_output_tokens: AgentParameterValue; + top_p: AgentParameterValue; + frequency_penalty: AgentParameterValue; + presence_penalty: AgentParameterValue; }; +export interface AgentToolResources { + execute_code?: ExecuteCodeResource; + file_search?: AgentFileSearchResource; +} +export interface ExecuteCodeResource { + /** + * A list of file IDs made available to the `execute_code` tool. + * There can be a maximum of 20 files associated with the tool. + */ + file_ids?: Array; +} + +export interface AgentFileSearchResource { + /** + * The ID of the vector store attached to this agent. There + * can be a maximum of 1 vector store attached to the agent. + */ + vector_store_ids?: Array; + /** + * A list of file IDs made available to the `file_search` tool. + * To be used before vector stores are implemented. + */ + file_ids?: Array; +} + export type Agent = { id: string; name: string | null; + author?: string | null; + authorName?: string | null; description: string | null; created_at: number; avatar: AgentAvatar | null; - file_ids: string[]; instructions: string | null; tools?: string[]; projectIds?: string[]; tool_kwargs?: Record; - tool_resources?: ToolResources; metadata?: Record; provider: AgentProvider; model: string | null; model_parameters: AgentModelParameters; - object: string; + conversation_starters?: string[]; + isCollaborative?: boolean; + tool_resources?: AgentToolResources; }; export type TAgentsMap = Record; @@ -197,6 +228,7 @@ export type AgentUpdateParams = { model_parameters?: AgentModelParameters; projectIds?: string[]; removeProjectIds?: string[]; + isCollaborative?: boolean; }; export type AgentListParams = { diff --git a/packages/data-provider/src/types/files.ts b/packages/data-provider/src/types/files.ts index 47a6e7c212..49ae79a313 100644 --- a/packages/data-provider/src/types/files.ts +++ b/packages/data-provider/src/types/files.ts @@ -7,6 +7,7 @@ export enum FileSources { openai = 'openai', s3 = 's3', vectordb = 'vectordb', + execute_code = 'execute_code', } export const checkOpenAIStorage = (source: string) => @@ -15,7 +16,9 @@ export const checkOpenAIStorage = (source: string) => export enum FileContext { avatar = 'avatar', unknown = 'unknown', + agents = 'agents', assistants = 'assistants', + execute_code = 'execute_code', image_generation = 'image_generation', assistants_output = 'assistants_output', message_attachment = 'message_attachment', @@ -128,6 +131,7 @@ export type BatchFile = { export type DeleteFilesBody = { files: BatchFile[]; + agent_id?: string; assistant_id?: string; tool_resource?: EToolResources; };