🚧 chore: merge latest dev build (#4288)

* 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
This commit is contained in:
Danny Avila 2024-09-30 17:17:57 -04:00 committed by GitHub
parent f33e75e2ee
commit ad74350036
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
123 changed files with 3611 additions and 1541 deletions

View file

@ -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(

View file

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

View file

@ -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

View file

@ -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,
};

View file

@ -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';

View file

@ -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');

View file

@ -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;

View file

@ -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 */

View file

@ -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. */

View file

@ -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;

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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<Agent|null>} 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<Agent>} 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<Agent>} 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<Agent>} 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<MongoAgent>} 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,
};

View file

@ -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',

View file

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

View file

@ -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",

View file

@ -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<UsageMetadata>} options.collectedUsage - The list of collected usage metadata.
* @returns {Record<string, t.EventHandler>} The default handlers.
* @throws {Error} If the request is not found.
*/
function getDefaultHandlers({ res, aggregateContent, collectedUsage }) {
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<MongoFile | { filename: string; filepath: string; expires: number;} | null>[]} 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,
};

View file

@ -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<UsageMetadata>} */
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',

View file

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

View file

@ -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);

View file

@ -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<Assistant>} The updated assistant.
*/
const addResourceFileId = async ({ req, openai, assistant_id, tool_resource, file_id }) => {

View file

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

View file

@ -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.

View file

@ -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());

View file

@ -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<ReadableStream> | 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';

View file

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

View file

@ -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;

View file

@ -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<UsageMetadata>} */
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]),

View file

@ -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' },
);
};

View file

@ -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<AxiosResponse>} 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 };

View file

@ -0,0 +1,5 @@
const crud = require('./crud');
module.exports = {
...crud,
};

View file

@ -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<MongoFile & { messageId: string, toolCallId: string } | { filename: string; filepath: string; expiresAt: number; conversationId: string; toolCallId: string; messageId: string } | undefined>} 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,
};

View file

@ -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<void>}
*/
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,
};

View file

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

View file

@ -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<string, StructuredTool>}>} 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) => {

View file

@ -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) {

View file

@ -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<MongoFile | { filename: string; filepath: string; expires: number;} | null>[]} 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

View file

@ -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;

View file

@ -194,6 +194,7 @@ export type AgentModelPanelProps = {
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
providers: Option[];
models: Record<string, string[]>;
agent_id?: string;
};
export type AugmentedColumnDef<TData, TValue> = ColumnDef<TData, TValue> & DataColumnMeta;
@ -430,6 +431,8 @@ export type Option = Record<string, unknown> & {
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;
}
}
}

View file

@ -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<string, ExtendedFile>;
files: Map<string, ExtendedFile> | undefined;
setFiles: React.Dispatch<React.SetStateAction<Map<string, ExtendedFile>>>;
setFilesLoading: React.Dispatch<React.SetStateAction<boolean>>;
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 (
<Image
key={index}
url={file.preview || file.filepath}
url={file.preview ?? file.filepath}
onDelete={handleDelete}
progress={file.progress}
source={file.source}

View file

@ -1,18 +1,20 @@
import { useMemo } from 'react';
import { EModelEndpoint, isAssistantsEndpoint, Constants } from 'librechat-data-provider';
import { EModelEndpoint, Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type * as t from 'librechat-data-provider';
import type { ReactNode } from 'react';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { useGetAssistantDocsQuery } from '~/data-provider';
import ConvoIcon from '~/components/Endpoints/ConvoIcon';
import { getIconEndpoint, getEntity, cn } from '~/utils';
import { useLocalize, useSubmitMessage } from '~/hooks';
import { TooltipAnchor } from '~/components/ui';
import { BirthdayIcon } from '~/components/svg';
import { getIconEndpoint, cn } from '~/utils';
import ConvoStarter from './ConvoStarter';
export default function Landing({ Header }: { Header?: ReactNode }) {
const { conversation } = useChatContext();
const agentsMap = useAgentsMapContext();
const assistantMap = useAssistantsMapContext();
const { data: startupConfig } = useGetStartupConfig();
const { data: endpointsConfig } = useGetEndpointsQuery();
@ -20,7 +22,6 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
const localize = useLocalize();
let { endpoint = '' } = conversation ?? {};
const { assistant_id = null } = conversation ?? {};
if (
endpoint === EModelEndpoint.chatGPTBrowser ||
@ -36,20 +37,32 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
select: (data) => 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 (
<div className="relative h-full">
<div className="absolute left-0 right-0">{Header != null ? Header : null}</div>
<div className="flex h-full flex-col items-center justify-center">
<div className={cn('relative h-12 w-12', assistantName && avatar ? 'mb-0' : 'mb-3')}>
<div className={cn('relative h-12 w-12', name && avatar ? 'mb-0' : 'mb-3')}>
<ConvoIcon
conversation={conversation}
agentsMap={agentsMap}
assistantMap={assistantMap}
conversation={conversation}
endpointsConfig={endpointsConfig}
containerClassName={containerClassName}
context="landing"
@ -80,11 +111,11 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
</TooltipAnchor>
) : null}
</div>
{assistantName ? (
{name ? (
<div className="flex flex-col items-center gap-0 p-2">
<div className="text-center text-2xl font-medium dark:text-white">{assistantName}</div>
<div className="text-center text-2xl font-medium dark:text-white">{name}</div>
<div className="max-w-md text-center text-sm font-normal text-text-primary ">
{assistantDesc ? assistantDesc : localize('com_nav_welcome_message')}
{description ? description : localize('com_nav_welcome_message')}
</div>
{/* <div className="mt-1 flex items-center gap-1 text-token-text-tertiary">
<div className="text-sm text-token-text-tertiary">By Daniel Avila</div>
@ -92,16 +123,14 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
</div>
) : (
<h2 className="mb-5 max-w-[75vh] px-12 text-center text-lg font-medium dark:text-white md:px-0 md:text-2xl">
{isAssistant
? conversation?.greeting ?? localize('com_nav_welcome_assistant')
: conversation?.greeting ?? localize('com_nav_welcome_message')}
{getWelcomeMessage()}
</h2>
)}
<div className="mt-8 flex flex-wrap justify-center gap-3 px-4">
{conversation_starters.length > 0 &&
conversation_starters
.slice(0, Constants.MAX_CONVO_STARTERS)
.map((text, index) => (
.map((text: string, index: number) => (
<ConvoStarter
key={index}
text={text}

View file

@ -1,6 +1,6 @@
import { EModelEndpoint } from 'librechat-data-provider';
import type { IconMapProps, AgentIconMapProps } from '~/common';
import { BrainCircuit } from 'lucide-react';
import { Feather } from 'lucide-react';
import {
MinimalPlugin,
GPTIcon,
@ -17,7 +17,13 @@ import {
import UnknownIcon from './UnknownIcon';
import { cn } from '~/utils';
const AssistantAvatar = ({ className = '', assistantName, avatar, size }: IconMapProps) => {
const AssistantAvatar = ({
className = '',
assistantName = '',
avatar = '',
context,
size,
}: IconMapProps) => {
if (assistantName && avatar) {
return (
<img
@ -32,10 +38,10 @@ const AssistantAvatar = ({ className = '', assistantName, avatar, size }: IconMa
return <AssistantIcon className={cn('text-token-secondary', className)} size={size} />;
}
return <Sparkles className={cn(assistantName === '' ? 'icon-2xl' : '', className)} />;
return <Sparkles className={cn(context === 'landing' ? 'icon-2xl' : '', className)} />;
};
const AgentAvatar = ({ className = '', agentName, avatar, size }: AgentIconMapProps) => {
const AgentAvatar = ({ className = '', avatar = '', agentName, size }: AgentIconMapProps) => {
if (agentName && avatar) {
return (
<img
@ -46,11 +52,9 @@ const AgentAvatar = ({ className = '', agentName, avatar, size }: AgentIconMapPr
height="80"
/>
);
} else if (agentName) {
return <AssistantIcon className={cn('text-token-secondary', className)} size={size} />;
}
return <BrainCircuit className={cn(agentName === '' ? 'icon-2xl' : '', className)} />;
return <Feather className={cn(agentName === '' ? 'icon-2xl' : '', className)} size={size} />;
};
const Bedrock = ({ className = '' }: IconMapProps) => {

View file

@ -113,7 +113,7 @@ const MenuItem: FC<MenuItemProps> = ({
<div className="flex grow items-center justify-between gap-2">
<div>
<div className="flex items-center gap-2">
{Icon && (
{Icon != null && (
<Icon
size={18}
endpoint={endpoint}

View file

@ -1,31 +1,38 @@
import { alternateName } from 'librechat-data-provider';
import { Content, Portal, Root } from '@radix-ui/react-popover';
import { alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { FC } from 'react';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { mapEndpoints, getEntity } from '~/utils';
import EndpointItems from './Endpoints/MenuItems';
import TitleButton from './UI/TitleButton';
import { mapEndpoints } from '~/utils';
const EndpointsMenu: FC = () => {
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 (
<Root>
@ -44,7 +51,7 @@ const EndpointsMenu: FC = () => {
<Content
side="bottom"
align="start"
className="mt-2 max-h-[65vh] min-w-[340px] overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white lg:max-h-[75vh]"
className="mt-2 max-h-[65vh] min-w-[340px] overflow-y-auto rounded-lg border border-border-light bg-header-primary text-text-primary shadow-lg lg:max-h-[75vh]"
>
<EndpointItems endpoints={endpoints} selected={endpoint} />
</Content>

View file

@ -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<string, unknown>[];
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({
<MarkdownLite content={code ? `\`\`\`python\n${code}\n\`\`\`` : ''} />
{logs && (
<div className="bg-gray-700 p-4 text-xs">
<div className="mb-1 text-gray-400">Result</div>
<div className="mb-1 text-gray-400">{localize('com_ui_result')}</div>
<div
className="prose flex flex-col-reverse text-white"
style={{
@ -78,91 +79,3 @@ export default function CodeAnalyze({
</>
);
}
const CodeInProgress = ({
offset,
circumference,
radius,
isSubmitting,
progress,
}: {
progress: number;
offset: number;
circumference: number;
radius: number;
isSubmitting: boolean;
}) => {
if (progress < 1 && !isSubmitting) {
return <CancelledIcon />;
}
return (
<div
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-white"
style={{ opacity: 1, transform: 'none' }}
data-projection-id="77"
>
<div className="absolute bottom-[1.5px] right-[1.5px]">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 20 20"
width="20"
height="20"
style={{ transform: 'translate3d(0px, 0px, 0px)' }}
preserveAspectRatio="xMidYMid meet"
>
<defs>
<clipPath id="__lottie_element_11">
<rect width="20" height="20" x="0" y="0" />
</clipPath>
</defs>
<g clipPath="url(#__lottie_element_11)">
<g
style={{ display: 'block', transform: 'matrix(1,0,0,1,-2,-2)', opacity: 1 }}
className="slide-from-left"
>
<g opacity="1" transform="matrix(1,0,0,1,7.026679992675781,8.834091186523438)">
<path
fill="rgb(177,98,253)"
fillOpacity="1"
d=" M1.2870399951934814,0.2207774966955185 C0.992609977722168,-0.07359249889850616 0.5152599811553955,-0.07359249889850616 0.22082999348640442,0.2207774966955185 C-0.07361000031232834,0.5151575207710266 -0.07361000031232834,0.992437481880188 0.22082999348640442,1.2868175506591797 C0.8473266959190369,1.9131841659545898 1.4738233089447021,2.53955078125 2.1003201007843018,3.16591739654541 C1.4738233089447021,3.7922842502593994 0.8473266959190369,4.4186506271362305 0.22082999348640442,5.045017719268799 C-0.07361000031232834,5.339417457580566 -0.07361000031232834,5.816617488861084 0.22082999348640442,6.11101770401001 C0.5152599811553955,6.405417442321777 0.992609977722168,6.405417442321777 1.2870399951934814,6.11101770401001 C2.091266632080078,5.306983947753906 2.895493268966675,4.502950668334961 3.6997199058532715,3.6989173889160156 C3.994119882583618,3.404517412185669 3.994119882583618,2.927217483520508 3.6997199058532715,2.6329174041748047 C2.895493268966675,1.8288708925247192 2.091266632080078,1.0248241424560547 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
fillOpacity="0"
stroke="rgb(177,98,253)"
strokeOpacity="1"
strokeWidth="0.201031"
d=" M1.2870399951934814,0.2207774966955185 C0.992609977722168,-0.07359249889850616 0.5152599811553955,-0.07359249889850616 0.22082999348640442,0.2207774966955185 C-0.07361000031232834,0.5151575207710266 -0.07361000031232834,0.992437481880188 0.22082999348640442,1.2868175506591797 C0.8473266959190369,1.9131841659545898 1.4738233089447021,2.53955078125 2.1003201007843018,3.16591739654541 C1.4738233089447021,3.7922842502593994 0.8473266959190369,4.4186506271362305 0.22082999348640442,5.045017719268799 C-0.07361000031232834,5.339417457580566 -0.07361000031232834,5.816617488861084 0.22082999348640442,6.11101770401001 C0.5152599811553955,6.405417442321777 0.992609977722168,6.405417442321777 1.2870399951934814,6.11101770401001 C2.091266632080078,5.306983947753906 2.895493268966675,4.502950668334961 3.6997199058532715,3.6989173889160156 C3.994119882583618,3.404517412185669 3.994119882583618,2.927217483520508 3.6997199058532715,2.6329174041748047 C2.895493268966675,1.8288708925247192 2.091266632080078,1.0248241424560547 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185"
/>
</g>
</g>
<g
style={{ display: 'block', transform: 'matrix(1,0,0,1,-2,-2)', opacity: 1 }}
className="slide-to-down"
>
<g opacity="1" transform="matrix(1,0,0,1,11.79640007019043,13.512199401855469)">
<path
fill="rgb(177,98,253)"
fillOpacity="1"
d=" M4.3225998878479,0 C3.1498000621795654,0 1.9769999980926514,0 0.8041999936103821,0 C0.36010000109672546,0 0,0.36000001430511475 0,0.804099977016449 C0,1.2482000589370728 0.36010000109672546,1.6081000566482544 0.8041999936103821,1.6081000566482544 C1.9769999980926514,1.6081000566482544 3.1498000621795654,1.6081000566482544 4.3225998878479,1.6081000566482544 C4.7667999267578125,1.6081000566482544 5.126800060272217,1.2482000589370728 5.126800060272217,0.804099977016449 C5.126800060272217,0.36000001430511475 4.7667999267578125,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
fillOpacity="0"
stroke="rgb(177,98,253)"
strokeOpacity="1"
strokeWidth="0.100515"
d=" M4.3225998878479,0 C3.1498000621795654,0 1.9769999980926514,0 0.8041999936103821,0 C0.36010000109672546,0 0,0.36000001430511475 0,0.804099977016449 C0,1.2482000589370728 0.36010000109672546,1.6081000566482544 0.8041999936103821,1.6081000566482544 C1.9769999980926514,1.6081000566482544 3.1498000621795654,1.6081000566482544 4.3225998878479,1.6081000566482544 C4.7667999267578125,1.6081000566482544 5.126800060272217,1.2482000589370728 5.126800060272217,0.804099977016449 C5.126800060272217,0.36000001430511475 4.7667999267578125,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0"
/>
</g>
</g>
</g>
</svg>
</div>
<ProgressCircle radius={radius} circumference={circumference} offset={offset} />
</div>
);
};

View file

@ -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<TMessageContentParts | undefined> | 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) => (
<Part
key={`display-${messageId}-${idx}`}
part={part}
isSubmitting={isSubmitting}
showCursor={idx === content.length - 1 && isLast}
messageId={messageId}
isCreatedByUser={isCreatedByUser}
/>
))}
.map((part, idx) => {
const toolCallId =
(part?.[ContentTypes.TOOL_CALL] as Agents.ToolCall | undefined)?.id ?? '';
const attachments = attachmentMap[toolCallId];
return (
<Part
part={part}
isSubmitting={isSubmitting}
attachments={attachments}
key={`display-${messageId}-${idx}`}
showCursor={idx === content.length - 1 && isLast}
messageId={messageId}
isCreatedByUser={isCreatedByUser}
/>
);
})}
</>
);
},

View file

@ -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 <ErrorMessage text={part[ContentTypes.TEXT].value} className="my-2" />;
} 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 (
<Container>
<Text
text={text}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
);
} 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 <ErrorMessage text={part[ContentTypes.TEXT].value} className="my-2" />;
} 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 (
<ToolCall
args={toolCall.args ?? ''}
name={toolCall.name ?? ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
/>
<Container>
<Text
text={text}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
return (
<CodeAnalyze
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
toolCall.type === ToolCallTypes.RETRIEVAL ||
toolCall.type === ToolCallTypes.FILE_SEARCH
) {
return (
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
);
} else if (
toolCall.type === ToolCallTypes.FUNCTION &&
ToolCallTypes.FUNCTION in toolCall &&
imageGenTools.has(toolCall.function.name)
) {
return (
<ImageGen
initialProgress={toolCall.progress ?? 0.1}
args={toolCall.function.arguments as string}
/>
);
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<Container>
<Text
text={''}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
);
}
} 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 (
<ExecuteCode
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
/>
);
} else if (isToolCall) {
return (
<ToolCall
args={toolCall.args ?? ''}
name={toolCall.name ?? ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
/>
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
return (
<CodeAnalyze
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
toolCall.type === ToolCallTypes.RETRIEVAL ||
toolCall.type === ToolCallTypes.FILE_SEARCH
) {
return (
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
);
} else if (
toolCall.type === ToolCallTypes.FUNCTION &&
ToolCallTypes.FUNCTION in toolCall &&
imageGenTools.has(toolCall.function.name)
) {
return (
<ImageGen
initialProgress={toolCall.progress ?? 0.1}
args={toolCall.function.arguments as string}
/>
);
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<Container>
<Text
text={''}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
);
}
return null;
}
return (
<ToolCall
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
args={toolCall.function.arguments as string}
name={toolCall.function.name}
output={toolCall.function.output}
/>
);
}
} else if (part.type === ContentTypes.IMAGE_FILE) {
const imageFile = part[ContentTypes.IMAGE_FILE];
const height = imageFile.height ?? 1920;
const width = imageFile.width ?? 1080;
return (
<ToolCall
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
args={toolCall.function.arguments as string}
name={toolCall.function.name}
output={toolCall.function.output}
<Image
imagePath={imageFile.filepath}
height={height}
width={width}
altText={imageFile.filename ?? 'Uploaded Image'}
placeholderDimensions={{
height: height + 'px',
width: width + 'px',
}}
/>
);
}
} else if (part.type === ContentTypes.IMAGE_FILE) {
const imageFile = part[ContentTypes.IMAGE_FILE];
const height = imageFile.height ?? 1920;
const width = imageFile.width ?? 1080;
return (
<Image
imagePath={imageFile.filepath}
height={height}
width={width}
altText={imageFile.filename ?? 'Uploaded Image'}
placeholderDimensions={{
height: height + 'px',
width: width + 'px',
}}
/>
);
}
return null;
});
return null;
},
);
export default Part;

View file

@ -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 <CancelledIcon />;
}
return (
<div
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-white"
style={{ opacity: 1, transform: 'none' }}
data-projection-id="77"
>
<div className="absolute bottom-[1.5px] right-[1.5px]">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 20 20"
width="20"
height="20"
style={{ transform: 'translate3d(0px, 0px, 0px)' }}
preserveAspectRatio="xMidYMid meet"
>
<defs>
<clipPath id="__lottie_element_11">
<rect width="20" height="20" x="0" y="0" />
</clipPath>
</defs>
<g clipPath="url(#__lottie_element_11)">
<g
style={{ display: 'block', transform: 'matrix(1,0,0,1,-2,-2)', opacity: 1 }}
className="slide-from-left"
>
<g opacity="1" transform="matrix(1,0,0,1,7.026679992675781,8.834091186523438)">
<path
fill="rgb(177,98,253)"
fillOpacity="1"
d=" M1.2870399951934814,0.2207774966955185 C0.992609977722168,-0.07359249889850616 0.5152599811553955,-0.07359249889850616 0.22082999348640442,0.2207774966955185 C-0.07361000031232834,0.5151575207710266 -0.07361000031232834,0.992437481880188 0.22082999348640442,1.2868175506591797 C0.8473266959190369,1.9131841659545898 1.4738233089447021,2.53955078125 2.1003201007843018,3.16591739654541 C1.4738233089447021,3.7922842502593994 0.8473266959190369,4.4186506271362305 0.22082999348640442,5.045017719268799 C-0.07361000031232834,5.339417457580566 -0.07361000031232834,5.816617488861084 0.22082999348640442,6.11101770401001 C0.5152599811553955,6.405417442321777 0.992609977722168,6.405417442321777 1.2870399951934814,6.11101770401001 C2.091266632080078,5.306983947753906 2.895493268966675,4.502950668334961 3.6997199058532715,3.6989173889160156 C3.994119882583618,3.404517412185669 3.994119882583618,2.927217483520508 3.6997199058532715,2.6329174041748047 C2.895493268966675,1.8288708925247192 2.091266632080078,1.0248241424560547 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
fillOpacity="0"
stroke="rgb(177,98,253)"
strokeOpacity="1"
strokeWidth="0.201031"
d=" M1.2870399951934814,0.2207774966955185 C0.992609977722168,-0.07359249889850616 0.5152599811553955,-0.07359249889850616 0.22082999348640442,0.2207774966955185 C-0.07361000031232834,0.5151575207710266 -0.07361000031232834,0.992437481880188 0.22082999348640442,1.2868175506591797 C0.8473266959190369,1.9131841659545898 1.4738233089447021,2.53955078125 2.1003201007843018,3.16591739654541 C1.4738233089447021,3.7922842502593994 0.8473266959190369,4.4186506271362305 0.22082999348640442,5.045017719268799 C-0.07361000031232834,5.339417457580566 -0.07361000031232834,5.816617488861084 0.22082999348640442,6.11101770401001 C0.5152599811553955,6.405417442321777 0.992609977722168,6.405417442321777 1.2870399951934814,6.11101770401001 C2.091266632080078,5.306983947753906 2.895493268966675,4.502950668334961 3.6997199058532715,3.6989173889160156 C3.994119882583618,3.404517412185669 3.994119882583618,2.927217483520508 3.6997199058532715,2.6329174041748047 C2.895493268966675,1.8288708925247192 2.091266632080078,1.0248241424560547 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185"
/>
</g>
</g>
<g
style={{ display: 'block', transform: 'matrix(1,0,0,1,-2,-2)', opacity: 1 }}
className="slide-to-down"
>
<g opacity="1" transform="matrix(1,0,0,1,11.79640007019043,13.512199401855469)">
<path
fill="rgb(177,98,253)"
fillOpacity="1"
d=" M4.3225998878479,0 C3.1498000621795654,0 1.9769999980926514,0 0.8041999936103821,0 C0.36010000109672546,0 0,0.36000001430511475 0,0.804099977016449 C0,1.2482000589370728 0.36010000109672546,1.6081000566482544 0.8041999936103821,1.6081000566482544 C1.9769999980926514,1.6081000566482544 3.1498000621795654,1.6081000566482544 4.3225998878479,1.6081000566482544 C4.7667999267578125,1.6081000566482544 5.126800060272217,1.2482000589370728 5.126800060272217,0.804099977016449 C5.126800060272217,0.36000001430511475 4.7667999267578125,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
fillOpacity="0"
stroke="rgb(177,98,253)"
strokeOpacity="1"
strokeWidth="0.100515"
d=" M4.3225998878479,0 C3.1498000621795654,0 1.9769999980926514,0 0.8041999936103821,0 C0.36010000109672546,0 0,0.36000001430511475 0,0.804099977016449 C0,1.2482000589370728 0.36010000109672546,1.6081000566482544 0.8041999936103821,1.6081000566482544 C1.9769999980926514,1.6081000566482544 3.1498000621795654,1.6081000566482544 4.3225998878479,1.6081000566482544 C4.7667999267578125,1.6081000566482544 5.126800060272217,1.2482000589370728 5.126800060272217,0.804099977016449 C5.126800060272217,0.36000001430511475 4.7667999267578125,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0"
/>
</g>
</g>
</g>
</svg>
</div>
<ProgressCircle radius={radius} circumference={circumference} offset={offset} />
</div>
);
};

View file

@ -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 (
<>
<div className="my-2.5 flex items-center gap-2.5">
<div className="relative h-5 w-5 shrink-0">
{progress < 1 ? (
<CodeInProgress
offset={offset}
radius={radius}
progress={progress}
isSubmitting={isSubmitting}
circumference={circumference}
/>
) : (
<FinishedIcon />
)}
</div>
<ProgressText
progress={progress}
onClick={() => setShowCode((prev) => !prev)}
inProgressText="Analyzing"
finishedText="Finished analyzing"
hasInput={!!code.length}
/>
</div>
{showCode && (
<div className="code-analyze-block mb-3 mt-0.5 overflow-hidden rounded-xl bg-black">
<MarkdownLite content={code ? `\`\`\`${lang}\n${code}\n\`\`\`` : ''} />
{output.length > 0 && (
<div className="bg-gray-700 p-4 text-xs">
<div
className="prose flex flex-col-reverse text-white"
style={{
color: 'white',
}}
>
<pre className="shrink-0">
<LogContent output={output} attachments={attachments} />
</pre>
</div>
</div>
)}
</div>
)}
{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 (
<Image
key={index}
altText={attachment.filename}
imagePath={filepath}
height={height}
width={width}
/>
);
}
})}
</>
);
}

View file

@ -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<LogContentProps> = ({ 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 (
<LogLink href={file.filepath} filename={file.filename}>
{'- '}
{file.filename} {localize('com_click_to_download')}
</LogLink>
);
};
return (
<>
{processedContent && <div>{processedContent}</div>}
{nonImageAttachments.length > 0 && (
<div>
<p>{localize('com_generated_files')}</p>
{nonImageAttachments.map((file, index) => (
<React.Fragment key={file.filepath}>
{renderAttachment(file)}
{index < nonImageAttachments.length - 1 && ', '}
</React.Fragment>
))}
</div>
)}
</>
);
};
export default LogContent;

View file

@ -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<LogLinkProps> = ({ href, filename, children }) => {
const { showToast } = useToastContext();
const { refetch: downloadFile } = useCodeOutputDownload(href);
const handleDownload = async (event: React.MouseEvent<HTMLAnchorElement>) => {
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 (
<a
href={href}
onClick={handleDownload}
target="_blank"
rel="noopener noreferrer"
className="!text-blue-400 visited:!text-purple-400 hover:underline"
>
{children}
</a>
);
};
export default LogLink;

View file

@ -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 (
<InProgressCall progress={progress} isSubmitting={isSubmitting} error={error}>
<div
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-white"
style={{ opacity: 1, transform: 'none' }}
data-projection-id="849"
>
<div>
<WrenchIcon />
</div>
<ProgressCircle radius={radius} circumference={circumference} offset={offset} />
</div>
</InProgressCall>
);
}
return error === true ? <CancelledIcon /> : <FinishedIcon />;
};
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 (
<Popover.Root>
<div className="my-2.5 flex items-center gap-2.5">
<div className="relative h-5 w-5 shrink-0">
{progress < 1 ? (
<InProgressCall progress={progress} isSubmitting={isSubmitting} error={error}>
<div
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-white"
style={{ opacity: 1, transform: 'none' }}
data-projection-id="849"
>
<div>
<WrenchIcon />
</div>
<ProgressCircle radius={radius} circumference={circumference} offset={offset} />
</div>
</InProgressCall>
) : error ? (
<CancelledIcon />
) : (
<FinishedIcon />
)}
</div>
<div className="relative h-5 w-5 shrink-0">{renderIcon()}</div>
<ProgressText
progress={progress}
onClick={() => ({})}
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 && (
<ToolPopover input={args} output={output} domain={domain} function_name={function_name} />
<ToolPopover
input={args ?? ''}
output={output}
domain={domain ?? ''}
function_name={function_name}
/>
)}
</div>
</Popover.Root>

View file

@ -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') ? (
<ConvoIconURL
preset={conversation}
endpointIconURL={endpointIconURL}
assistantName={assistantName}
assistantAvatar={avatar}
assistantName={name}
agentAvatar={avatar}
agentName={name}
context={context}
/>
) : (
<div className={containerClassName}>
{endpoint &&
Icon &&
Icon({
size,
context,
className,
iconURL: endpointIconURL,
assistantName,
endpoint,
avatar,
})}
{endpoint && Icon != null && (
<Icon
size={size}
context={context}
endpoint={endpoint}
className={className}
iconURL={endpointIconURL}
assistantName={name}
agentName={name}
avatar={avatar}
/>
)}
</div>
)}
</>

View file

@ -64,7 +64,7 @@ const ConvoIconURL: React.FC<ConvoIconURLProps> = ({
</div>
);
return <Icon />;
return <Icon context={context} />;
}
return (
@ -73,10 +73,10 @@ const ConvoIconURL: React.FC<ConvoIconURLProps> = ({
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}
/>
</div>
);

View file

@ -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<IconProps> = (props) => {
) : (
<div className="h-6 w-6">
<div className="shadow-stroke flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<BrainCircuit className="h-2/3 w-2/3 text-gray-400" />
<Feather className="h-2/3 w-2/3 text-gray-400" />
</div>
</div>
),

View file

@ -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<IconProps> = (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<IconProps> = (props) => {
[EModelEndpoint.assistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
[EModelEndpoint.azureAssistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
[EModelEndpoint.agents]: {
icon: <BrainCircuit className="icon-sm" />,
icon: <Feather className="icon-sm" />,
name: props.modelLabel ?? alternateName[EModelEndpoint.agents],
},
[EModelEndpoint.bedrock]: {
@ -57,21 +57,14 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
name: props.modelLabel ?? alternateName[EModelEndpoint.bedrock],
},
default: {
icon: (
<UnknownIcon
iconURL={props.iconURL}
endpoint={endpoint}
className="icon-sm"
context="nav"
/>
),
icon: <UnknownIcon iconURL={iconURL} endpoint={endpoint} className="icon-sm" context="nav" />,
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 (

View file

@ -138,6 +138,7 @@ const ContentRender = memo(
enterEdit={enterEdit}
siblingIdx={siblingIdx}
setSiblingIdx={setSiblingIdx}
attachments={msg.attachments}
/>
</div>
</div>

View file

@ -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<number>(1);
const [input, setInput] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const lastSeenCreatedId = useRef<string | null>(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<AgentListResponse>([
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<AgentListResponse>([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<HTMLInputElement>): 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',

View file

@ -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<TPlugin[]>([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({
</div>
</button>
</div>
<CapabilitiesForm
codeEnabled={codeEnabled}
agentsConfig={agentsConfig}
retrievalEnabled={false}
/>
{/* File Search */}
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
{/* Agent Tools & Actions */}
<div className="mb-6">
<label className={labelClass}>
{`${toolsEnabled === true ? localize('com_assistants_tools') : ''}
{`${toolsEnabled === true ? localize('com_ui_tools') : ''}
${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''}
${actionsEnabled === true ? localize('com_assistants_actions') : ''}`}
</label>
@ -344,11 +374,14 @@ export default function AgentConfig({
setCurrentAgentId={setCurrentAgentId}
createMutation={create}
/>
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
projectIds={agent?.projectIds ?? []}
/>
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) && (
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
projectIds={agent?.projectIds ?? []}
isCollaborative={agent?.isCollaborative}
/>
)}
{/* Submit Button */}
<button
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"

View file

@ -3,15 +3,20 @@ import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import { Controller, useWatch, useForm, FormProvider } from 'react-hook-form';
import {
Tools,
SystemRoles,
EModelEndpoint,
isAssistantsEndpoint,
defaultAgentFormValues,
} from 'librechat-data-provider';
import type { TConfig } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps, Option } from '~/common';
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
import { useSelectAgent, useLocalize } from '~/hooks';
// import CapabilitiesForm from './CapabilitiesForm';
import type { AgentForm, AgentPanelProps, StringOption } from '~/common';
import {
useCreateAgentMutation,
useUpdateAgentMutation,
useGetAgentByIdQuery,
} from '~/data-provider';
import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks';
import AgentPanelSkeleton from './AgentPanelSkeleton';
import { createProviderOption } from '~/utils';
import { useToastContext } from '~/Providers';
import AgentConfig from './AgentConfig';
@ -29,11 +34,17 @@ export default function AgentPanel({
agentsConfig,
endpointsConfig,
}: AgentPanelProps & { agentsConfig?: TConfig | null }) {
const { onSelect: onSelectAgent } = useSelectAgent();
const { showToast } = useToastContext();
const localize = useLocalize();
const { user } = useAuthContext();
const { showToast } = useToastContext();
const { onSelect: onSelectAgent } = useSelectAgent();
const modelsQuery = useGetModelsQuery();
const agentQuery = useGetAgentByIdQuery(current_agent_id ?? '', {
enabled: !!(current_agent_id ?? ''),
});
const models = useMemo(() => modelsQuery.data ?? {}, [modelsQuery.data]);
const methods = useForm<AgentForm>({
defaultValues: defaultAgentFormValues,
@ -81,7 +92,7 @@ export default function AgentPanel({
onSuccess: (data) => {
setCurrentAgentId(data.id);
showToast({
message: `${localize('com_assistants_create_success ')} ${
message: `${localize('com_assistants_create_success')} ${
data.name ?? localize('com_ui_agent')
}`,
});
@ -101,23 +112,25 @@ export default function AgentPanel({
(data: AgentForm) => {
const tools = data.tools ?? [];
if (data.code_interpreter) {
tools.push(Tools.code_interpreter);
if (data.execute_code === true) {
tools.push(Tools.execute_code);
}
if (data.retrieval) {
if (data.file_search === true) {
tools.push(Tools.file_search);
}
const {
name,
model,
model_parameters,
provider: _provider,
description,
instructions,
model: _model,
model_parameters,
provider: _provider,
} = data;
const provider = typeof _provider === 'string' ? _provider : (_provider as Option).value;
const model = _model ?? '';
const provider =
(typeof _provider === 'string' ? _provider : (_provider as StringOption).value) ?? '';
if (agent_id) {
update.mutate({
@ -135,6 +148,13 @@ export default function AgentPanel({
return;
}
if (!provider || !model) {
return showToast({
message: localize('com_agents_missing_provider_model'),
status: 'error',
});
}
create.mutate({
name,
description,
@ -145,7 +165,7 @@ export default function AgentPanel({
model_parameters,
});
},
[agent_id, create, update],
[agent_id, create, update, showToast, localize],
);
const handleSelectAgent = useCallback(() => {
@ -154,6 +174,15 @@ export default function AgentPanel({
}
}, [agent_id, onSelectAgent]);
if (agentQuery.isInitialLoading) {
return <AgentPanelSkeleton />;
}
const canEditAgent =
agentQuery.data?.isCollaborative ?? false
? true
: agentQuery.data?.author === user?.id || user?.role === SystemRoles.ADMIN;
return (
<FormProvider {...methods}>
<form
@ -169,6 +198,7 @@ export default function AgentPanel({
<AgentSelect
reset={reset}
value={field.value}
agentQuery={agentQuery}
setCurrentAgentId={setCurrentAgentId}
selectedAgentId={current_agent_id ?? null}
createMutation={create}
@ -188,10 +218,25 @@ export default function AgentPanel({
</button>
)}
</div>
{activePanel === Panel.model ? (
<ModelPanel setActivePanel={setActivePanel} providers={providers} models={models} />
) : null}
{activePanel === Panel.builder ? (
{!canEditAgent && (
<div className="flex h-[30vh] w-full items-center justify-center">
<div className="text-center">
<h2 className="text-token-text-primary m-2 text-xl font-semibold">
{localize('com_agents_not_available')}
</h2>
<p className="text-token-text-secondary">{localize('com_agents_no_access')}</p>
</div>
</div>
)}
{canEditAgent && activePanel === Panel.model && (
<ModelPanel
setActivePanel={setActivePanel}
agent_id={agent_id}
providers={providers}
models={models}
/>
)}
{canEditAgent && activePanel === Panel.builder && (
<AgentConfig
actions={actions}
setAction={setAction}
@ -200,7 +245,7 @@ export default function AgentPanel({
endpointsConfig={endpointsConfig}
setCurrentAgentId={setCurrentAgentId}
/>
) : null}
)}
</form>
</FormProvider>
);

View file

@ -0,0 +1,70 @@
import React from 'react';
import { Skeleton } from '~/components/ui';
export default function AgentPanelSkeleton() {
return (
<div className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden">
{/* Agent Select and Button */}
<div className="mt-1 flex w-full gap-2">
<Skeleton className="h-[40px] w-3/4 rounded" />
<Skeleton className="h-[40px] w-1/4 rounded" />
</div>
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
{/* Avatar */}
<div className="mb-4">
<div className="flex w-full items-center justify-center gap-4">
<Skeleton className="relative h-20 w-20 rounded-full" />
</div>
{/* Name */}
<Skeleton className="mb-2 h-5 w-1/5 rounded" />
<Skeleton className="mb-1 h-[40px] w-full rounded" />
<Skeleton className="h-3 w-1/4 rounded" />
</div>
{/* Description */}
<div className="mb-4">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="h-[40px] w-full rounded" />
</div>
{/* Instructions */}
<div className="mb-6">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="h-[100px] w-full rounded" />
</div>
{/* Model and Provider */}
<div className="mb-6">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="h-[40px] w-full rounded" />
</div>
{/* Capabilities */}
<div className="mb-6">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="mb-2 h-[40px] w-full rounded" />
<Skeleton className="h-[40px] w-full rounded" />
</div>
{/* Tools & Actions */}
<div className="mb-6">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="mb-2 h-[40px] w-full rounded" />
<Skeleton className="mb-2 h-[40px] w-full rounded" />
<div className="flex space-x-2">
<Skeleton className="h-8 w-1/2 rounded" />
<Skeleton className="h-8 w-1/2 rounded" />
</div>
</div>
{/* Bottom Buttons */}
<div className="flex items-center justify-end gap-2">
<Skeleton className="h-[40px] w-[100px] rounded" />
<Skeleton className="h-[40px] w-[100px] rounded" />
<Skeleton className="h-[40px] w-[100px] rounded" />
</div>
</div>
</div>
);
}

View file

@ -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]);

View file

@ -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<AgentForm>;
value?: TAgentOption;
selectedAgentId: string | null;
agentQuery: QueryObserverResult<Agent>;
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
createMutation: UseMutationResult<Agent, Error, AgentCreateParams>;
}) {
const localize = useLocalize();
// TODO: file handling for agents
// const fileMap = useFileMapContext();
const lastSelectedAgent = useRef<string | null>(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 ? <EarthIcon className={'icon-lg text-green-400'} /> : 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<AgentForm & AgentCapabilities> = {
...actions,
const agentTools: string[] = [];
(fullAgent.tools ?? []).forEach((tool) => {
if (capabilities[tool] !== undefined) {
capabilities[tool] = true;
return;
}
agentTools.push(tool);
});
const formValues: Partial<AgentForm & TAgentCapabilities> = {
...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(

View file

@ -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<AgentForm>();
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<AgentForm>();
// 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 (
<div className="mb-4">
@ -50,10 +46,10 @@ export default function CapabilitiesForm({
</span>
</div>
<div className="flex flex-col items-start gap-2">
{codeEnabled && <Code />}
{retrievalEnabled && <Retrieval retrievalModels={retrievalModels} />}
{codeEnabled === true && <Code />}
{retrievalEnabled === true && <Retrieval retrievalModels={retrievalModels} />}
{/* {imageVisionEnabled && version == 1 && <ImageVision />} */}
{codeEnabled && <CodeFiles agent_id={agent_id} files={files} />}
{/* {codeEnabled && <CodeFiles agent_id={agent_id} files={files} />} */}
</div>
</div>
);

View file

@ -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() {
<HoverCard openDelay={50}>
<div className="flex items-center">
<Controller
name={Capabilities.code_interpreter}
name={AgentCapabilities.execute_code}
control={control}
render={({ field }) => (
<Checkbox
@ -30,30 +30,34 @@ export default function Code() {
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
value={field.value.toString()}
/>
)}
/>
<div className="flex items-center space-x-2">
<button
type="button"
className="flex items-center space-x-2"
onClick={() =>
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
setValue(AgentCapabilities.execute_code, !getValues(AgentCapabilities.execute_code), {
shouldDirty: true,
})
}
>
<label
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={Capabilities.code_interpreter}
onClick={() =>
setValue(Capabilities.code_interpreter, !getValues(Capabilities.code_interpreter), {
shouldDirty: true,
})
}
htmlFor={AgentCapabilities.execute_code}
>
{localize('com_assistants_code_interpreter')}
{localize('com_agents_execute_code')}
</label>
<HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
</HoverCardTrigger>
</div>
</button>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-gray-600 dark:text-gray-300">
<p className="text-sm text-text-secondary">
{/* // TODO: add a Code Interpreter description */}
</p>
</div>

View file

@ -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<AgentForm>();
const fileInputRef = useRef<HTMLInputElement>(null);
const [files, setFiles] = useState<Map<string, ExtendedFile>>(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 (
<div className="mb-6">
<div className="mb-1.5 flex items-center gap-2">
<span>
<label className="text-token-text-primary block font-medium">
{localize('com_assistants_file_search')}
</label>
</span>
</div>
<FileSearchCheckbox />
<div className="flex flex-col gap-2">
<div>
<button
type="button"
disabled={!agent_id || fileSearchChecked === false}
className="btn btn-neutral border-token-border-light relative h-8 rounded-lg font-medium"
onClick={handleButtonClick}
>
<div className="flex w-full items-center justify-center gap-1">
<AttachmentIcon className="text-token-text-primary h-4 w-4" />
<input
multiple={true}
type="file"
style={{ display: 'none' }}
tabIndex={-1}
ref={fileInputRef}
disabled={!agent_id || fileSearchChecked === false}
onChange={handleFileChange}
/>
{localize('com_ui_upload_files')}
</div>
</button>
</div>
{/* Disabled Message */}
{agent_id ? null : (
<div className="text-sm text-text-secondary">
{localize('com_agents_file_search_disabled')}
</div>
)}
{/* Knowledge Files */}
<FileRow
files={files}
setFiles={setFiles}
setFilesLoading={setFilesLoading}
agent_id={agent_id}
tool_resource={EToolResources.file_search}
fileFilter={(file: ExtendedFile) =>
retrievalMimeTypes.some((regex) => regex.test(file.type ?? ''))
}
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
/>
</div>
</div>
);
}

View file

@ -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<AgentForm>();
const { control, setValue, getValues } = methods;
return (
<>
<HoverCard openDelay={50}>
<div className="my-2 flex items-center">
<Controller
name={AgentCapabilities.file_search}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value.toString()}
/>
)}
/>
<button
type="button"
className="flex items-center space-x-2"
onClick={() =>
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
setValue(AgentCapabilities.file_search, !getValues(AgentCapabilities.file_search), {
shouldDirty: true,
})
}
>
<label
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={AgentCapabilities.file_search}
>
{localize('com_agents_enable_file_search')}
</label>
<HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
</HoverCardTrigger>
</button>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{localize('com_agents_file_search_info')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</div>
</HoverCard>
</>
);
}

View file

@ -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<AgentForm>();
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 (
<div className="h-full overflow-auto px-2 pb-12 text-sm">
<div className="scrollbar-gutter-stable h-full min-h-[50vh] overflow-auto pb-12 text-sm">
<div className="model-panel relative flex flex-col items-center px-16 py-6 text-center">
<div className="absolute left-0 top-6">
<button
@ -56,228 +87,125 @@ export default function ModelPanel({
<div className="mb-2 mt-2 text-xl font-medium">{localize('com_ui_model_parameters')}</div>
</div>
{/* Endpoint aka Provider for Agents */}
<div className="mb-4">
<label
className="text-token-text-primary model-panel-label mb-2 block font-medium"
htmlFor="provider"
>
{localize('com_ui_provider')} <span className="text-red-500">*</span>
</label>
<Controller
name="provider"
control={control}
rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => (
<>
<SelectDropDown
emptyTitle={true}
value={field.value ?? ''}
placeholder={localize('com_ui_select_provider')}
setValue={field.onChange}
availableValues={providers}
showAbove={false}
showLabel={false}
className={cn(
cardStyle,
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer',
!field.value && 'border-2 border-yellow-400',
<div className="p-2">
{/* Endpoint aka Provider for Agents */}
<div className="mb-4">
<label
className="text-token-text-primary model-panel-label mb-2 block font-medium"
htmlFor="provider"
>
{localize('com_ui_provider')} <span className="text-red-500">*</span>
</label>
<Controller
name="provider"
control={control}
rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => (
<>
<SelectDropDown
emptyTitle={true}
value={field.value ?? ''}
placeholder={localize('com_ui_select_provider')}
setValue={field.onChange}
availableValues={providers}
showAbove={false}
showLabel={false}
className={cn(
cardStyle,
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer',
(field.value === undefined || field.value === '') &&
'border-2 border-yellow-400',
)}
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
/>
{error && (
<span className="model-panel-error text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)}
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
/>
{error && (
<span className="model-panel-error text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)}
</>
)}
/>
</div>
{/* Model */}
<div className="model-panel-section mb-6">
<label
className={cn(
'text-token-text-primary model-panel-label mb-2 block font-medium',
!provider && 'text-gray-500 dark:text-gray-400',
)}
htmlFor="model"
>
{localize('com_ui_model')} <span className="text-red-500">*</span>
</label>
<Controller
name="model"
control={control}
rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => (
<>
<SelectDropDown
emptyTitle={true}
placeholder={
provider
? localize('com_ui_select_model')
: localize('com_ui_select_provider_first')
}
value={field.value}
setValue={field.onChange}
availableValues={models}
showAbove={false}
showLabel={false}
disabled={!provider}
className={cn(
cardStyle,
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4',
!provider ? 'cursor-not-allowed bg-gray-200' : 'hover:cursor-pointer',
</>
)}
/>
</div>
{/* Model */}
<div className="model-panel-section mb-4">
<label
className={cn(
'text-token-text-primary model-panel-label mb-2 block font-medium',
!provider && 'text-gray-500 dark:text-gray-400',
)}
htmlFor="model"
>
{localize('com_ui_model')} <span className="text-red-500">*</span>
</label>
<Controller
name="model"
control={control}
rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => (
<>
<SelectDropDown
emptyTitle={true}
placeholder={
provider
? localize('com_ui_select_model')
: localize('com_ui_select_provider_first')
}
value={field.value}
setValue={field.onChange}
availableValues={models}
showAbove={false}
showLabel={false}
disabled={!provider}
className={cn(
cardStyle,
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4',
!provider ? 'cursor-not-allowed bg-gray-200' : 'hover:cursor-pointer',
)}
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
/>
{provider && error && (
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)}
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
/>
{provider && error && (
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)}
</>
)}
/>
</div>
<div className="mb-4">
<Controller
name="model_parameters.temperature"
control={control}
rules={{ required: false }}
render={({ field }) => (
<>
<ModelParameters
label="com_endpoint_temperature"
ariaLabel="Temperature"
min={-2}
max={2}
step={0.01}
stepClick={0.01}
initialValue={field.value ?? 1}
onChange={field.onChange}
showButtons={true}
disabled={!provider}
/>
</>
)}
/>
</div>
<div className="mb-4">
<Controller
name="model_parameters.max_context_tokens"
control={control}
rules={{ required: false }}
render={({ field }) => (
<>
<ModelParameters
label="com_endpoint_max_output_tokens"
ariaLabel="Max Context Tokens"
min={0}
max={4096}
step={1}
stepClick={1}
initialValue={field.value ?? 0}
onChange={field.onChange}
showButtons={true}
disabled={!provider}
/>
</>
)}
/>
</div>
<div className="mb-4">
<Controller
name="model_parameters.max_output_tokens"
control={control}
rules={{ required: false }}
render={({ field }) => (
<>
<ModelParameters
label="com_endpoint_context_tokens"
ariaLabel="Max Context Tokens"
min={0}
max={4096}
step={1}
stepClick={1}
initialValue={field.value ?? 0}
onChange={field.onChange}
showButtons={true}
disabled={!provider}
/>
</>
)}
/>
</div>
<div className="mb-4">
<Controller
name="model_parameters.top_p"
control={control}
rules={{ required: false }}
render={({ field }) => (
<>
<ModelParameters
label="com_endpoint_top_p"
ariaLabel="Top P"
min={-2}
max={2}
step={0.01}
stepClick={0.01}
initialValue={field.value ?? 1}
onChange={field.onChange}
showButtons={true}
disabled={!provider}
/>
</>
)}
/>
</div>
<div className="mb-4">
<Controller
name="model_parameters.frequency_penalty"
control={control}
rules={{ required: false }}
render={({ field }) => (
<>
<ModelParameters
label="com_endpoint_frequency_penalty"
ariaLabel="Frequency Penalty"
min={-2}
max={2}
step={0.01}
stepClick={0.01}
initialValue={field.value ?? 0}
onChange={field.onChange}
showButtons={true}
disabled={!provider}
/>
</>
)}
/>
</div>
<div className="mb-4">
<Controller
name="model_parameters.presence_penalty"
control={control}
rules={{ required: false }}
render={({ field }) => (
<>
<ModelParameters
label="com_endpoint_presence_penalty"
ariaLabel="Presence Penalty"
min={-2}
max={2}
step={0.01}
stepClick={0.01}
initialValue={field.value ?? 0}
onChange={field.onChange}
showButtons={true}
disabled={!provider}
/>
</>
)}
/>
</>
)}
/>
</div>
</div>
{/* Model Parameters */}
{parameters && (
<div className="h-auto max-w-full overflow-x-hidden p-2">
<div className="grid grid-cols-4 gap-6">
{' '}
{/* 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 (
<Component
key={key}
settingKey={key}
defaultValue={defaultValue}
{...rest}
setOption={setOption as t.TSetOption}
conversation={modelParameters as Partial<t.TConversation>}
/>
);
})}
</div>
</div>
)}
</div>
);
}

View file

@ -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"
>
<div className="flex w-full items-center justify-center gap-2 text-blue-500">
<div className="flex items-center justify-center gap-2 text-blue-500">
<Share2Icon className="icon-md h-4 w-4" />
</div>
</button>
</OGDialogTrigger>
<OGDialogContent className="border-border-light bg-surface-primary-alt text-text-secondary">
<OGDialogContent className="w-1/4 border-border-light bg-surface-primary-alt text-text-secondary">
<OGDialogTitle>
{localize(
'com_ui_share_var',
@ -133,11 +161,12 @@ export default function ShareAgent({
handleSubmit(onSubmit)(e);
}}
>
<div className="mb-4 flex items-center justify-between gap-2 py-4">
<div className="flex items-center justify-between gap-2 py-2">
<div className="flex items-center">
<button
type="button"
className="mr-2 cursor-pointer"
disabled={isFetching || updateAgent.isLoading || !instanceProjectId}
onClick={() =>
setValue(Permissions.SHARED_GLOBAL, !getValues(Permissions.SHARED_GLOBAL), {
shouldDirty: true,
@ -166,18 +195,54 @@ export default function ShareAgent({
name={Permissions.SHARED_GLOBAL}
control={control}
disabled={isFetching || updateAgent.isLoading || !instanceProjectId}
rules={{
validate: (value) => {
const isValid = !(value && agentIsGlobal);
if (!isValid) {
showToast({
message: localize('com_ui_agent_already_shared_to_all'),
status: 'warning',
render={({ field }) => (
<Switch
{...field}
checked={field.value}
onCheckedChange={field.onChange}
value={field.value.toString()}
/>
)}
/>
</div>
<div className="mb-4 flex items-center justify-between gap-2 py-2">
<div className="flex items-center">
<button
type="button"
className="mr-2 cursor-pointer"
disabled={
isFetching || updateAgent.isLoading || !instanceProjectId || !sharedGlobalValue
}
onClick={() =>
setValue(Permissions.UPDATE, !getValues(Permissions.UPDATE), {
shouldDirty: true,
})
}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setValue(Permissions.UPDATE, !getValues(Permissions.UPDATE), {
shouldDirty: true,
});
}
return isValid;
},
}}
}}
aria-checked={getValues(Permissions.UPDATE)}
role="checkbox"
>
{localize('com_agents_allow_editing')}
</button>
{/* <label htmlFor={Permissions.UPDATE} className="select-none">
{agentIsGlobal && (
<span className="ml-2 text-xs">{localize('com_ui_agent_editing_allowed')}</span>
)}
</label> */}
</div>
<Controller
name={Permissions.UPDATE}
control={control}
disabled={
isFetching || updateAgent.isLoading || !instanceProjectId || !sharedGlobalValue
}
render={({ field }) => (
<Switch
{...field}

View file

@ -389,7 +389,7 @@ export default function AssistantPanel({
{/* Tools */}
<div className="mb-6">
<label className={labelClass}>
{`${toolsEnabled === true ? localize('com_assistants_tools') : ''}
{`${toolsEnabled === true ? localize('com_ui_tools') : ''}
${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''}
${actionsEnabled === true ? localize('com_assistants_actions') : ''}`}
</label>

View file

@ -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 (
<div
data-collapsed={isCollapsed}
className="bg-token-sidebar-surface-primary hide-scrollbar group flex-shrink-0 overflow-x-hidden py-2 data-[collapsed=true]:py-2"
className="bg-token-sidebar-surface-primary hide-scrollbar group flex-shrink-0 overflow-x-hidden"
>
<div className="h-full">
<div className="flex h-full min-h-0 flex-col">
@ -76,12 +75,11 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
>
<link.icon className="mr-2 h-4 w-4" />
{localize(link.title)}
{link.label && (
{link.label != null && link.label && (
<span
className={cn(
'ml-auto transition-all duration-300 ease-in-out',
'ml-auto opacity-100 transition-all duration-300 ease-in-out',
variant === 'default' ? 'text-background dark:text-white' : '',
isCollapsed ? 'opacity-0' : 'opacity-100',
)}
>
{link.label}

View file

@ -572,3 +572,12 @@ export const presetSettings: Record<
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneralColumns,
};
export const agentSettings: Record<string, SettingsConfiguration | undefined> = Object.entries(
presetSettings,
).reduce((acc, [key, value]) => {
if (value) {
acc[key] = value.col2;
}
return acc;
}, {});

View file

@ -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 (
<>
<AssistantSwitcher {...props} />
<Separator className="max-w-[98%] bg-surface-tertiary" />
</>
);
return <AssistantSwitcher {...props} />;
} else if (isAgentsEndpoint(props.endpoint) && props.endpointKeyProvided) {
return (
<>
<AgentSwitcher {...props} />
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
</>
);
return <AgentSwitcher {...props} />;
} else if (isAssistantsEndpoint(props.endpoint)) {
return null;
}
return (
<>
<ModelSwitcher {...props} />
<Separator className="max-w-[98%] bg-surface-tertiary" />
</>
);
return <ModelSwitcher {...props} />;
}

View file

@ -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({
<div className="flex items-center">
<div className="text-center sm:text-left">
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{localize('com_nav_tool_dialog')}
{isAgentTools
? localize('com_nav_tool_dialog_agents')
: localize('com_nav_tool_dialog')}
</DialogTitle>
<Description className="text-sm text-gray-500 dark:text-gray-300">
{localize('com_nav_tool_dialog_description')}

View file

@ -1,10 +1,10 @@
import { cn } from '~/utils';
export default function Sparkles({ className = '' }) {
export default function Sparkles({ className = '', size = 24 }) {
return (
<svg
width="24"
height="24"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"

View file

@ -0,0 +1 @@
export * from './queries';

View file

@ -0,0 +1,84 @@
import { QueryKeys, dataService } from 'librechat-data-provider';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import { addFileToCache } from '~/utils';
export const useGetFiles = <TData = t.TFile[] | boolean>(
config?: UseQueryOptions<t.TFile[], unknown, TData>,
): QueryObserverResult<TData, unknown> => {
return useQuery<t.TFile[], unknown, TData>([QueryKeys.files], () => dataService.getFiles(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
});
};
export const useGetFileConfig = <TData = t.FileConfig>(
config?: UseQueryOptions<t.FileConfig, unknown, TData>,
): QueryObserverResult<TData, unknown> => {
return useQuery<t.FileConfig, unknown, TData>(
[QueryKeys.fileConfig],
() => dataService.getFileConfig(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
export const useFileDownload = (userId?: string, file_id?: string): QueryObserverResult<string> => {
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<string> => {
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,
},
);
};

View file

@ -1,3 +1,4 @@
export * from './Files';
export * from './connection';
export * from './mutations';
export * from './prompts';

View file

@ -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<t.ConversationData>([QueryKeys.allConversations], handleDelete);
queryClient.setQueryData<t.ConversationData>(
[QueryKeys.archivedConversations],
handleDelete,
);
const current = queryClient.getQueryData<t.ConversationData>([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<t.TFile[] | undefined>([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<t.Agent>([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<t.TFile[] | undefined>([QueryKeys.files], (cachefiles) => {
const { files: filesDeleted } = args[0];
@ -1228,6 +1264,8 @@ export const useUpdateAgentMutation = (
return agent;
}),
});
queryClient.setQueryData<t.Agent>([QueryKeys.agent, variables.agent_id], updatedAgent);
return options?.onSuccess?.(updatedAgent, variables, context);
},
},

View file

@ -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 = <TData = TFile[] | boolean>(
config?: UseQueryOptions<TFile[], unknown, TData>,
): QueryObserverResult<TData, unknown> => {
return useQuery<TFile[], unknown, TData>([QueryKeys.files], () => dataService.getFiles(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
});
};
export const useGetFileConfig = <TData = FileConfig>(
config?: UseQueryOptions<FileConfig, unknown, TData>,
): QueryObserverResult<TData, unknown> => {
return useQuery<FileConfig, unknown, TData>(
[QueryKeys.fileConfig],
() => dataService.getFileConfig(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
import { findPageForConversation } from '~/utils';
export const useGetPresetsQuery = (
config?: UseQueryOptions<TPreset[]>,
@ -321,7 +290,7 @@ export const useGetAssistantByIdQuery = (
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([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 = <TData = AssistantDocument[]>(
);
};
export const useFileDownload = (userId?: string, file_id?: string): QueryObserverResult<string> => {
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
*/

View file

@ -17,7 +17,7 @@ export default function useSelectAgent() {
);
const agentQuery = useGetAgentByIdQuery(selectedAgentId ?? '', {
enabled: !!selectedAgentId,
enabled: !!(selectedAgentId ?? ''),
});
const updateConversation = useCallback(

View file

@ -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<Map<string, ExtendedFile>>;
const useFileDeletion = ({
mutateAsync,
agent_id,
assistant_id,
tool_resource,
}: {
mutateAsync: UseMutateAsyncFunction<DeleteFilesResponse, unknown, DeleteFilesBody, unknown>;
mutateAsync: UseMutateAsyncFunction<t.DeleteFilesResponse, unknown, t.DeleteFilesBody, unknown>;
agent_id?: string;
assistant_id?: string;
tool_resource?: EToolResources;
}) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_batch, setFileDeleteBatch] = useState<BatchFile[]>([]);
const [_batch, setFileDeleteBatch] = useState<t.BatchFile[]>([]);
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 };

View file

@ -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<TEndpointsConfig>([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;
}

View file

@ -20,11 +20,7 @@ export default function useUpdateFiles(setFiles: FileSetter) {
});
};
const updateFileById = (
fileId: string,
updates: Partial<ExtendedFile>,
isAssistantFile?: boolean,
) => {
const updateFileById = (fileId: string, updates: Partial<ExtendedFile>, 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);
}

View file

@ -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';

View file

@ -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<string, TAttachment[] | undefined>)[messageId] || [];
return {
...prevMap,
[messageId]: [...messageAttachments, data],
};
});
};
}

View file

@ -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<TMessage | undefined>
).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<TMessage | undefined>
).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<ConversationData>([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<ConversationData>([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<TMessage>) => {
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,
};
}

View file

@ -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);

View file

@ -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: 'أدوات المساعدين',
},

View file

@ -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',
},

View file

@ -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',
},

View file

@ -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',

View file

@ -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',
},

View file

@ -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',

View file

@ -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',
},

View file

@ -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',
},

View file

@ -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',
},

View file

@ -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: '도구',
},

View file

@ -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: 'Инструменты',
},

View file

@ -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',

Some files were not shown because too many files have changed in this diff Show more