Compare commits
19 Commits
v0.7.7
...
feat/LaTeX
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47dff7d387 | ||
|
|
cf03731cc8 | ||
|
|
cbd5bd2405 | ||
|
|
ded3cd8876 | ||
|
|
9db00edfc4 | ||
|
|
a53638c481 | ||
|
|
d6ab769b80 | ||
|
|
3e3dfe5bad | ||
|
|
cc661c95ee | ||
|
|
6ea88e09a2 | ||
|
|
a846e898a2 | ||
|
|
dc8d5dee6a | ||
|
|
f04ae65a75 | ||
|
|
0a4da06fe1 | ||
|
|
932474c44e | ||
|
|
a2b7812033 | ||
|
|
88d2920b06 | ||
|
|
c5e012abc0 | ||
|
|
b51cd21b3c |
3
.github/workflows/backend-review.yml
vendored
3
.github/workflows/backend-review.yml
vendored
@@ -39,6 +39,9 @@ jobs:
|
||||
- name: Install MCP Package
|
||||
run: npm run build:mcp
|
||||
|
||||
- name: Install Data Schemas Package
|
||||
run: npm run build:data-schemas
|
||||
|
||||
- name: Create empty auth.json file
|
||||
run: |
|
||||
mkdir -p api/data
|
||||
|
||||
58
.github/workflows/data-schemas.yml
vendored
Normal file
58
.github/workflows/data-schemas.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Publish `@librechat/data-schemas` to NPM
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/data-schemas/package.json'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
required: false
|
||||
default: 'Manual publish requested'
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd packages/data-schemas && npm ci
|
||||
|
||||
- name: Build
|
||||
run: cd packages/data-schemas && npm run build
|
||||
|
||||
- name: Set up npm authentication
|
||||
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.PUBLISH_NPM_TOKEN }}" > ~/.npmrc
|
||||
|
||||
- name: Check version change
|
||||
id: check
|
||||
working-directory: packages/data-schemas
|
||||
run: |
|
||||
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
||||
PUBLISHED_VERSION=$(npm view @librechat/data-schemas version 2>/dev/null || echo "0.0.0")
|
||||
if [ "$PACKAGE_VERSION" = "$PUBLISHED_VERSION" ]; then
|
||||
echo "No version change, skipping publish"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Version changed, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Pack package
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
working-directory: packages/data-schemas
|
||||
run: npm pack
|
||||
|
||||
- name: Publish
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
working-directory: packages/data-schemas
|
||||
run: npm publish *.tgz --access public
|
||||
@@ -84,11 +84,11 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
sign-commits: true
|
||||
commit-message: "chore: update CHANGELOG for release ${GITHUB_REF##*/}"
|
||||
commit-message: "chore: update CHANGELOG for release ${{ github.ref_name }}"
|
||||
base: main
|
||||
branch: "changelog/${GITHUB_REF##*/}"
|
||||
branch: "changelog/${{ github.ref_name }}"
|
||||
reviewers: danny-avila
|
||||
title: "chore: update CHANGELOG for release ${GITHUB_REF##*/}"
|
||||
title: "chore: update CHANGELOG for release ${{ github.ref_name }}"
|
||||
body: |
|
||||
**Description**:
|
||||
- This PR updates the CHANGELOG.md by removing the "Unreleased" section and adding new release notes for release ${GITHUB_REF##*/} above previous releases.
|
||||
- This PR updates the CHANGELOG.md by removing the "Unreleased" section and adding new release notes for release ${{ github.ref_name }} above previous releases.
|
||||
@@ -11,6 +11,7 @@ RUN npm config set fetch-retry-maxtimeout 600000 && \
|
||||
COPY package*.json ./
|
||||
COPY packages/data-provider/package*.json ./packages/data-provider/
|
||||
COPY packages/mcp/package*.json ./packages/mcp/
|
||||
COPY packages/data-schemas/package*.json ./packages/data-schemas/
|
||||
COPY client/package*.json ./client/
|
||||
COPY api/package*.json ./api/
|
||||
|
||||
@@ -32,6 +33,13 @@ COPY packages/mcp ./
|
||||
COPY --from=data-provider-build /app/packages/data-provider/dist /app/packages/data-provider/dist
|
||||
RUN npm run build
|
||||
|
||||
# Build data-schemas
|
||||
FROM base AS data-schemas-build
|
||||
WORKDIR /app/packages/data-schemas
|
||||
COPY packages/data-schemas ./
|
||||
COPY --from=data-provider-build /app/packages/data-provider/dist /app/packages/data-provider/dist
|
||||
RUN npm run build
|
||||
|
||||
# Client build
|
||||
FROM base AS client-build
|
||||
WORKDIR /app/client
|
||||
@@ -49,8 +57,9 @@ COPY api ./api
|
||||
COPY config ./config
|
||||
COPY --from=data-provider-build /app/packages/data-provider/dist ./packages/data-provider/dist
|
||||
COPY --from=mcp-build /app/packages/mcp/dist ./packages/mcp/dist
|
||||
COPY --from=data-schemas-build /app/packages/data-schemas/dist ./packages/data-schemas/dist
|
||||
COPY --from=client-build /app/client/dist ./client/dist
|
||||
WORKDIR /app/api
|
||||
EXPOSE 3080
|
||||
ENV HOST=0.0.0.0
|
||||
CMD ["node", "server/index.js"]
|
||||
CMD ["node", "server/index.js"]
|
||||
@@ -197,6 +197,6 @@ We thank [Locize](https://locize.com) for their translation management tools tha
|
||||
|
||||
<p align="center">
|
||||
<a href="https://locize.com" target="_blank" rel="noopener noreferrer">
|
||||
<img src="https://locize.com/img/locize_color.svg" alt="Locize Logo" height="50">
|
||||
<img src="https://github.com/user-attachments/assets/d6b70894-6064-475e-bb65-92a9e23e0077" alt="Locize Logo" height="50">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -1121,9 +1121,13 @@ class BaseClient {
|
||||
return message;
|
||||
}
|
||||
|
||||
const files = await getFiles({
|
||||
file_id: { $in: fileIds },
|
||||
});
|
||||
const files = await getFiles(
|
||||
{
|
||||
file_id: { $in: fileIds },
|
||||
},
|
||||
{},
|
||||
{},
|
||||
);
|
||||
|
||||
await this.addImageURLs(message, files, this.visionMode);
|
||||
|
||||
|
||||
@@ -325,4 +325,37 @@ describe('formatAgentMessages', () => {
|
||||
);
|
||||
expect(result[0].content).not.toContain('Analyzing the problem...');
|
||||
});
|
||||
|
||||
it('should exclude ERROR type content parts', () => {
|
||||
const payload = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hello there' },
|
||||
{
|
||||
type: ContentTypes.ERROR,
|
||||
[ContentTypes.ERROR]:
|
||||
'An error occurred while processing the request: Something went wrong',
|
||||
},
|
||||
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Final answer' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = formatAgentMessages(payload);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBeInstanceOf(AIMessage);
|
||||
expect(result[0].content).toEqual([
|
||||
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hello there' },
|
||||
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Final answer' },
|
||||
]);
|
||||
|
||||
// Make sure no error content exists in the result
|
||||
const hasErrorContent = result[0].content.some(
|
||||
(item) =>
|
||||
item.type === ContentTypes.ERROR || JSON.stringify(item).includes('An error occurred'),
|
||||
);
|
||||
expect(hasErrorContent).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -211,6 +211,8 @@ const formatAgentMessages = (payload) => {
|
||||
} else if (part.type === ContentTypes.THINK) {
|
||||
hasReasoning = true;
|
||||
continue;
|
||||
} else if (part.type === ContentTypes.ERROR) {
|
||||
continue;
|
||||
} else {
|
||||
currentContent.push(part);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ const {
|
||||
} = require('../');
|
||||
const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process');
|
||||
const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch');
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
const { createMCPTool } = require('~/server/services/MCP');
|
||||
const { loadSpecs } = require('./loadSpecs');
|
||||
const { logger } = require('~/config');
|
||||
@@ -90,45 +91,6 @@ const validateTools = async (user, tools = []) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadAuthValues = async ({ userId, authFields, throwError = true }) => {
|
||||
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, throwError);
|
||||
} 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;
|
||||
};
|
||||
|
||||
/** @typedef {typeof import('@langchain/core/tools').Tool} ToolConstructor */
|
||||
/** @typedef {import('@langchain/core/tools').Tool} Tool */
|
||||
|
||||
@@ -348,7 +310,6 @@ const loadTools = async ({
|
||||
|
||||
module.exports = {
|
||||
loadToolWithAuth,
|
||||
loadAuthValues,
|
||||
validateTools,
|
||||
loadTools,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
const { validateTools, loadTools, loadAuthValues } = require('./handleTools');
|
||||
const { validateTools, loadTools } = require('./handleTools');
|
||||
const handleOpenAIErrors = require('./handleOpenAIErrors');
|
||||
|
||||
module.exports = {
|
||||
handleOpenAIErrors,
|
||||
loadAuthValues,
|
||||
validateTools,
|
||||
loadTools,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const axios = require('axios');
|
||||
const { EventSource } = require('eventsource');
|
||||
const { Time, CacheKeys } = require('librechat-data-provider');
|
||||
const logger = require('./winston');
|
||||
@@ -47,9 +48,46 @@ const sendEvent = (res, event) => {
|
||||
res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates and configures an Axios instance with optional proxy settings.
|
||||
*
|
||||
* @typedef {import('axios').AxiosInstance} AxiosInstance
|
||||
* @typedef {import('axios').AxiosProxyConfig} AxiosProxyConfig
|
||||
*
|
||||
* @returns {AxiosInstance} A configured Axios instance
|
||||
* @throws {Error} If there's an issue creating the Axios instance or parsing the proxy URL
|
||||
*/
|
||||
function createAxiosInstance() {
|
||||
const instance = axios.create();
|
||||
|
||||
if (process.env.proxy) {
|
||||
try {
|
||||
const url = new URL(process.env.proxy);
|
||||
|
||||
/** @type {AxiosProxyConfig} */
|
||||
const proxyConfig = {
|
||||
host: url.hostname.replace(/^\[|\]$/g, ''),
|
||||
protocol: url.protocol.replace(':', ''),
|
||||
};
|
||||
|
||||
if (url.port) {
|
||||
proxyConfig.port = parseInt(url.port, 10);
|
||||
}
|
||||
|
||||
instance.defaults.proxy = proxyConfig;
|
||||
} catch (error) {
|
||||
console.error('Error parsing proxy URL:', error);
|
||||
throw new Error(`Invalid proxy URL: ${process.env.proxy}`);
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
logger,
|
||||
sendEvent,
|
||||
getMCPManager,
|
||||
createAxiosInstance,
|
||||
getFlowStateManager,
|
||||
};
|
||||
|
||||
126
api/config/index.spec.js
Normal file
126
api/config/index.spec.js
Normal file
@@ -0,0 +1,126 @@
|
||||
const axios = require('axios');
|
||||
const { createAxiosInstance } = require('./index');
|
||||
|
||||
// Mock axios
|
||||
jest.mock('axios', () => ({
|
||||
interceptors: {
|
||||
request: { use: jest.fn(), eject: jest.fn() },
|
||||
response: { use: jest.fn(), eject: jest.fn() },
|
||||
},
|
||||
create: jest.fn().mockReturnValue({
|
||||
defaults: {
|
||||
proxy: null,
|
||||
},
|
||||
get: jest.fn().mockResolvedValue({ data: {} }),
|
||||
post: jest.fn().mockResolvedValue({ data: {} }),
|
||||
put: jest.fn().mockResolvedValue({ data: {} }),
|
||||
delete: jest.fn().mockResolvedValue({ data: {} }),
|
||||
}),
|
||||
get: jest.fn().mockResolvedValue({ data: {} }),
|
||||
post: jest.fn().mockResolvedValue({ data: {} }),
|
||||
put: jest.fn().mockResolvedValue({ data: {} }),
|
||||
delete: jest.fn().mockResolvedValue({ data: {} }),
|
||||
reset: jest.fn().mockImplementation(function () {
|
||||
this.get.mockClear();
|
||||
this.post.mockClear();
|
||||
this.put.mockClear();
|
||||
this.delete.mockClear();
|
||||
this.create.mockClear();
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('createAxiosInstance', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
jest.clearAllMocks();
|
||||
// Create a clean copy of process.env
|
||||
process.env = { ...originalEnv };
|
||||
// Default: no proxy
|
||||
delete process.env.proxy;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Restore original process.env
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
test('creates an axios instance without proxy when no proxy env is set', () => {
|
||||
const instance = createAxiosInstance();
|
||||
|
||||
expect(axios.create).toHaveBeenCalledTimes(1);
|
||||
expect(instance.defaults.proxy).toBeNull();
|
||||
});
|
||||
|
||||
test('configures proxy correctly with hostname and protocol', () => {
|
||||
process.env.proxy = 'http://example.com';
|
||||
|
||||
const instance = createAxiosInstance();
|
||||
|
||||
expect(axios.create).toHaveBeenCalledTimes(1);
|
||||
expect(instance.defaults.proxy).toEqual({
|
||||
host: 'example.com',
|
||||
protocol: 'http',
|
||||
});
|
||||
});
|
||||
|
||||
test('configures proxy correctly with hostname, protocol and port', () => {
|
||||
process.env.proxy = 'https://proxy.example.com:8080';
|
||||
|
||||
const instance = createAxiosInstance();
|
||||
|
||||
expect(axios.create).toHaveBeenCalledTimes(1);
|
||||
expect(instance.defaults.proxy).toEqual({
|
||||
host: 'proxy.example.com',
|
||||
protocol: 'https',
|
||||
port: 8080,
|
||||
});
|
||||
});
|
||||
|
||||
test('handles proxy URLs with authentication', () => {
|
||||
process.env.proxy = 'http://user:pass@proxy.example.com:3128';
|
||||
|
||||
const instance = createAxiosInstance();
|
||||
|
||||
expect(axios.create).toHaveBeenCalledTimes(1);
|
||||
expect(instance.defaults.proxy).toEqual({
|
||||
host: 'proxy.example.com',
|
||||
protocol: 'http',
|
||||
port: 3128,
|
||||
// Note: The current implementation doesn't handle auth - if needed, add this functionality
|
||||
});
|
||||
});
|
||||
|
||||
test('throws error when proxy URL is invalid', () => {
|
||||
process.env.proxy = 'invalid-url';
|
||||
|
||||
expect(() => createAxiosInstance()).toThrow('Invalid proxy URL');
|
||||
expect(axios.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// If you want to test the actual URL parsing more thoroughly
|
||||
test('handles edge case proxy URLs correctly', () => {
|
||||
// IPv6 address
|
||||
process.env.proxy = 'http://[::1]:8080';
|
||||
|
||||
let instance = createAxiosInstance();
|
||||
|
||||
expect(instance.defaults.proxy).toEqual({
|
||||
host: '::1',
|
||||
protocol: 'http',
|
||||
port: 8080,
|
||||
});
|
||||
|
||||
// URL with path (which should be ignored for proxy config)
|
||||
process.env.proxy = 'http://proxy.example.com:8080/some/path';
|
||||
|
||||
instance = createAxiosInstance();
|
||||
|
||||
expect(instance.defaults.proxy).toEqual({
|
||||
host: 'proxy.example.com',
|
||||
protocol: 'http',
|
||||
port: 8080,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
const { MeiliSearch } = require('meilisearch');
|
||||
const Conversation = require('~/models/schema/convoSchema');
|
||||
const Message = require('~/models/schema/messageSchema');
|
||||
const { Conversation } = require('~/models/Conversation');
|
||||
const { Message } = require('~/models/Message');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const mongoose = require('mongoose');
|
||||
const actionSchema = require('./schema/action');
|
||||
const { actionSchema } = require('@librechat/data-schemas');
|
||||
|
||||
const Action = mongoose.model('action', actionSchema);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const {
|
||||
removeAgentFromAllProjects,
|
||||
} = require('./Project');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const agentSchema = require('./schema/agent');
|
||||
const { agentSchema } = require('@librechat/data-schemas');
|
||||
|
||||
const Agent = mongoose.model('agent', agentSchema);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const mongoose = require('mongoose');
|
||||
const assistantSchema = require('./schema/assistant');
|
||||
const { assistantSchema } = require('@librechat/data-schemas');
|
||||
|
||||
const Assistant = mongoose.model('assistant', assistantSchema);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const mongoose = require('mongoose');
|
||||
const balanceSchema = require('./schema/balance');
|
||||
const { balanceSchema } = require('@librechat/data-schemas');
|
||||
const { getMultiplier } = require('./tx');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
const Banner = require('./schema/banner');
|
||||
const mongoose = require('mongoose');
|
||||
const logger = require('~/config/winston');
|
||||
const { bannerSchema } = require('@librechat/data-schemas');
|
||||
|
||||
const Banner = mongoose.model('Banner', bannerSchema);
|
||||
|
||||
/**
|
||||
* Retrieves the current active banner.
|
||||
* @returns {Promise<Object|null>} The active banner object or null if no active banner is found.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const { logger } = require('~/config');
|
||||
// const { Categories } = require('./schema/categories');
|
||||
|
||||
const options = [
|
||||
{
|
||||
|
||||
@@ -15,19 +15,6 @@ const searchConversation = async (conversationId) => {
|
||||
throw new Error('Error searching conversation');
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Searches for a conversation by conversationId and returns associated file ids.
|
||||
* @param {string} conversationId - The conversation's ID.
|
||||
* @returns {Promise<string[] | null>}
|
||||
*/
|
||||
const getConvoFiles = async (conversationId) => {
|
||||
try {
|
||||
return (await Conversation.findOne({ conversationId }, 'files').lean())?.files ?? [];
|
||||
} catch (error) {
|
||||
logger.error('[getConvoFiles] Error getting conversation files', error);
|
||||
throw new Error('Error getting conversation files');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a single conversation for a given user and conversation ID.
|
||||
@@ -73,9 +60,46 @@ const deleteNullOrEmptyConversations = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves files from a conversation that have either embedded=true
|
||||
* or a metadata.fileIdentifier. Simplified and efficient query.
|
||||
*
|
||||
* @param {string} conversationId - The conversation ID
|
||||
* @returns {Promise<MongoFile[]>} - Filtered array of matching file objects
|
||||
*/
|
||||
const getToolFiles = async (conversationId) => {
|
||||
try {
|
||||
const [result] = await Conversation.aggregate([
|
||||
{ $match: { conversationId } },
|
||||
{
|
||||
$project: {
|
||||
files: {
|
||||
$filter: {
|
||||
input: '$files',
|
||||
as: 'file',
|
||||
cond: {
|
||||
$or: [
|
||||
{ $eq: ['$$file.embedded', true] },
|
||||
{ $ifNull: ['$$file.metadata.fileIdentifier', false] },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
_id: 0,
|
||||
},
|
||||
},
|
||||
]).exec();
|
||||
|
||||
return result?.files || [];
|
||||
} catch (error) {
|
||||
logger.error('[getConvoEmbeddedFiles] Error fetching embedded files:', error);
|
||||
throw new Error('Error fetching embedded files');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Conversation,
|
||||
getConvoFiles,
|
||||
getToolFiles,
|
||||
searchConversation,
|
||||
deleteNullOrEmptyConversations,
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
const ConversationTag = require('./schema/conversationTagSchema');
|
||||
const mongoose = require('mongoose');
|
||||
const Conversation = require('./schema/convoSchema');
|
||||
const logger = require('~/config/winston');
|
||||
|
||||
const { conversationTagSchema } = require('@librechat/data-schemas');
|
||||
|
||||
const ConversationTag = mongoose.model('ConversationTag', conversationTagSchema);
|
||||
|
||||
/**
|
||||
* Retrieves all conversation tags for a user.
|
||||
* @param {string} user - The user ID.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const mongoose = require('mongoose');
|
||||
const fileSchema = require('./schema/fileSchema');
|
||||
const { fileSchema } = require('@librechat/data-schemas');
|
||||
|
||||
const File = mongoose.model('File', fileSchema);
|
||||
|
||||
@@ -7,7 +7,7 @@ const File = mongoose.model('File', fileSchema);
|
||||
* Finds a file by its file_id with additional query options.
|
||||
* @param {string} file_id - The unique identifier of the file.
|
||||
* @param {object} options - Query options for filtering, projection, etc.
|
||||
* @returns {Promise<MongoFile>} A promise that resolves to the file document or null.
|
||||
* @returns {Promise<IMongoFile>} A promise that resolves to the file document or null.
|
||||
*/
|
||||
const findFileById = async (file_id, options = {}) => {
|
||||
return await File.findOne({ file_id, ...options }).lean();
|
||||
@@ -17,18 +17,20 @@ const findFileById = async (file_id, options = {}) => {
|
||||
* Retrieves files matching a given filter, sorted by the most recently updated.
|
||||
* @param {Object} filter - The filter criteria to apply.
|
||||
* @param {Object} [_sortOptions] - Optional sort parameters.
|
||||
* @returns {Promise<Array<MongoFile>>} A promise that resolves to an array of file documents.
|
||||
* @param {Object|String} [selectFields={ text: 0 }] - Fields to include/exclude in the query results.
|
||||
* Default excludes the 'text' field.
|
||||
* @returns {Promise<Array<IMongoFile>>} A promise that resolves to an array of file documents.
|
||||
*/
|
||||
const getFiles = async (filter, _sortOptions) => {
|
||||
const getFiles = async (filter, _sortOptions, selectFields = { text: 0 }) => {
|
||||
const sortOptions = { updatedAt: -1, ..._sortOptions };
|
||||
return await File.find(filter).sort(sortOptions).lean();
|
||||
return await File.find(filter).select(selectFields).sort(sortOptions).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new file with a TTL of 1 hour.
|
||||
* @param {MongoFile} data - The file data to be created, must contain file_id.
|
||||
* @param {IMongoFile} data - The file data to be created, must contain file_id.
|
||||
* @param {boolean} disableTTL - Whether to disable the TTL.
|
||||
* @returns {Promise<MongoFile>} A promise that resolves to the created file document.
|
||||
* @returns {Promise<IMongoFile>} A promise that resolves to the created file document.
|
||||
*/
|
||||
const createFile = async (data, disableTTL) => {
|
||||
const fileData = {
|
||||
@@ -48,8 +50,8 @@ const createFile = async (data, disableTTL) => {
|
||||
|
||||
/**
|
||||
* Updates a file identified by file_id with new data and removes the TTL.
|
||||
* @param {MongoFile} data - The data to update, must contain file_id.
|
||||
* @returns {Promise<MongoFile>} A promise that resolves to the updated file document.
|
||||
* @param {IMongoFile} data - The data to update, must contain file_id.
|
||||
* @returns {Promise<IMongoFile>} A promise that resolves to the updated file document.
|
||||
*/
|
||||
const updateFile = async (data) => {
|
||||
const { file_id, ...update } = data;
|
||||
@@ -62,8 +64,8 @@ const updateFile = async (data) => {
|
||||
|
||||
/**
|
||||
* Increments the usage of a file identified by file_id.
|
||||
* @param {MongoFile} data - The data to update, must contain file_id and the increment value for usage.
|
||||
* @returns {Promise<MongoFile>} A promise that resolves to the updated file document.
|
||||
* @param {IMongoFile} data - The data to update, must contain file_id and the increment value for usage.
|
||||
* @returns {Promise<IMongoFile>} A promise that resolves to the updated file document.
|
||||
*/
|
||||
const updateFileUsage = async (data) => {
|
||||
const { file_id, inc = 1 } = data;
|
||||
@@ -77,7 +79,7 @@ const updateFileUsage = async (data) => {
|
||||
/**
|
||||
* Deletes a file identified by file_id.
|
||||
* @param {string} file_id - The unique identifier of the file to delete.
|
||||
* @returns {Promise<MongoFile>} A promise that resolves to the deleted file document or null.
|
||||
* @returns {Promise<IMongoFile>} A promise that resolves to the deleted file document or null.
|
||||
*/
|
||||
const deleteFile = async (file_id) => {
|
||||
return await File.findOneAndDelete({ file_id }).lean();
|
||||
@@ -86,7 +88,7 @@ const deleteFile = async (file_id) => {
|
||||
/**
|
||||
* Deletes a file identified by a filter.
|
||||
* @param {object} filter - The filter criteria to apply.
|
||||
* @returns {Promise<MongoFile>} A promise that resolves to the deleted file document or null.
|
||||
* @returns {Promise<IMongoFile>} A promise that resolves to the deleted file document or null.
|
||||
*/
|
||||
const deleteFileByFilter = async (filter) => {
|
||||
return await File.findOneAndDelete(filter).lean();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const mongoose = require('mongoose');
|
||||
const keySchema = require('./schema/key');
|
||||
const { keySchema } = require('@librechat/data-schemas');
|
||||
|
||||
module.exports = mongoose.model('Key', keySchema);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { model } = require('mongoose');
|
||||
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
|
||||
const projectSchema = require('~/models/schema/projectSchema');
|
||||
const { projectSchema } = require('@librechat/data-schemas');
|
||||
|
||||
const Project = model('Project', projectSchema);
|
||||
|
||||
@@ -9,7 +9,7 @@ const Project = model('Project', projectSchema);
|
||||
*
|
||||
* @param {string} projectId - The ID of the project to find and return as a plain object.
|
||||
* @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document.
|
||||
* @returns {Promise<MongoProject>} A plain object representing the project document, or `null` if no project is found.
|
||||
* @returns {Promise<IMongoProject>} A plain object representing the project document, or `null` if no project is found.
|
||||
*/
|
||||
const getProjectById = async function (projectId, fieldsToSelect = null) {
|
||||
const query = Project.findById(projectId);
|
||||
@@ -27,7 +27,7 @@ const getProjectById = async function (projectId, fieldsToSelect = null) {
|
||||
*
|
||||
* @param {string} projectName - The name of the project to find or create.
|
||||
* @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document.
|
||||
* @returns {Promise<MongoProject>} A plain object representing the project document.
|
||||
* @returns {Promise<IMongoProject>} A plain object representing the project document.
|
||||
*/
|
||||
const getProjectByName = async function (projectName, fieldsToSelect = null) {
|
||||
const query = { name: projectName };
|
||||
@@ -47,7 +47,7 @@ const getProjectByName = async function (projectName, fieldsToSelect = null) {
|
||||
*
|
||||
* @param {string} projectId - The ID of the project to update.
|
||||
* @param {string[]} promptGroupIds - The array of prompt group IDs to add to the project.
|
||||
* @returns {Promise<MongoProject>} The updated project document.
|
||||
* @returns {Promise<IMongoProject>} The updated project document.
|
||||
*/
|
||||
const addGroupIdsToProject = async function (projectId, promptGroupIds) {
|
||||
return await Project.findByIdAndUpdate(
|
||||
@@ -62,7 +62,7 @@ const addGroupIdsToProject = async function (projectId, promptGroupIds) {
|
||||
*
|
||||
* @param {string} projectId - The ID of the project to update.
|
||||
* @param {string[]} promptGroupIds - The array of prompt group IDs to remove from the project.
|
||||
* @returns {Promise<MongoProject>} The updated project document.
|
||||
* @returns {Promise<IMongoProject>} The updated project document.
|
||||
*/
|
||||
const removeGroupIdsFromProject = async function (projectId, promptGroupIds) {
|
||||
return await Project.findByIdAndUpdate(
|
||||
@@ -87,7 +87,7 @@ const removeGroupFromAllProjects = async (promptGroupId) => {
|
||||
*
|
||||
* @param {string} projectId - The ID of the project to update.
|
||||
* @param {string[]} agentIds - The array of agent IDs to add to the project.
|
||||
* @returns {Promise<MongoProject>} The updated project document.
|
||||
* @returns {Promise<IMongoProject>} The updated project document.
|
||||
*/
|
||||
const addAgentIdsToProject = async function (projectId, agentIds) {
|
||||
return await Project.findByIdAndUpdate(
|
||||
@@ -102,7 +102,7 @@ const addAgentIdsToProject = async function (projectId, agentIds) {
|
||||
*
|
||||
* @param {string} projectId - The ID of the project to update.
|
||||
* @param {string[]} agentIds - The array of agent IDs to remove from the project.
|
||||
* @returns {Promise<MongoProject>} The updated project document.
|
||||
* @returns {Promise<IMongoProject>} The updated project document.
|
||||
*/
|
||||
const removeAgentIdsFromProject = async function (projectId, agentIds) {
|
||||
return await Project.findByIdAndUpdate(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { SystemRoles, SystemCategories, Constants } = require('librechat-data-provider');
|
||||
const {
|
||||
@@ -6,10 +7,13 @@ const {
|
||||
removeGroupIdsFromProject,
|
||||
removeGroupFromAllProjects,
|
||||
} = require('./Project');
|
||||
const { Prompt, PromptGroup } = require('./schema/promptSchema');
|
||||
const { promptGroupSchema, promptSchema } = require('@librechat/data-schemas');
|
||||
const { escapeRegExp } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const PromptGroup = mongoose.model('PromptGroup', promptGroupSchema);
|
||||
const Prompt = mongoose.model('Prompt', promptSchema);
|
||||
|
||||
/**
|
||||
* Create a pipeline for the aggregation to get prompt groups
|
||||
* @param {Object} query
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const mongoose = require('mongoose');
|
||||
const {
|
||||
CacheKeys,
|
||||
SystemRoles,
|
||||
@@ -12,9 +13,11 @@ const {
|
||||
temporaryChatPermissionsSchema,
|
||||
} = require('librechat-data-provider');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const Role = require('~/models/schema/roleSchema');
|
||||
const { roleSchema } = require('@librechat/data-schemas');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const Role = mongoose.model('Role', roleSchema);
|
||||
|
||||
/**
|
||||
* Retrieve a role by name and convert the found role document to a plain object.
|
||||
* If the role with the given name doesn't exist and the name is a system defined role, create it and return the lean version.
|
||||
@@ -168,6 +171,7 @@ const initializeRoles = async function () {
|
||||
}
|
||||
};
|
||||
module.exports = {
|
||||
Role,
|
||||
getRoleByName,
|
||||
initializeRoles,
|
||||
updateRoleByName,
|
||||
|
||||
@@ -8,7 +8,7 @@ const {
|
||||
} = require('librechat-data-provider');
|
||||
const { updateAccessPermissions, initializeRoles } = require('~/models/Role');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const Role = require('~/models/schema/roleSchema');
|
||||
const { Role } = require('~/models/Role');
|
||||
|
||||
// Mock the cache
|
||||
jest.mock('~/cache/getLogStores', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const mongoose = require('mongoose');
|
||||
const signPayload = require('~/server/services/signPayload');
|
||||
const { hashToken } = require('~/server/utils/crypto');
|
||||
const sessionSchema = require('./schema/session');
|
||||
const { sessionSchema } = require('@librechat/data-schemas');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const Session = mongoose.model('Session', sessionSchema);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { nanoid } = require('nanoid');
|
||||
const { Constants } = require('librechat-data-provider');
|
||||
const { Conversation } = require('~/models/Conversation');
|
||||
const SharedLink = require('./schema/shareSchema');
|
||||
const { shareSchema } = require('@librechat/data-schemas');
|
||||
const SharedLink = mongoose.model('SharedLink', shareSchema);
|
||||
const { getMessages } = require('./Message');
|
||||
const logger = require('~/config/winston');
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { encryptV2 } = require('~/server/utils/crypto');
|
||||
const tokenSchema = require('./schema/tokenSchema');
|
||||
const { tokenSchema } = require('@librechat/data-schemas');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const ToolCall = require('./schema/toolCallSchema');
|
||||
const mongoose = require('mongoose');
|
||||
const { toolCallSchema } = require('@librechat/data-schemas');
|
||||
const ToolCall = mongoose.model('ToolCall', toolCallSchema);
|
||||
|
||||
/**
|
||||
* Create a new tool call
|
||||
* @param {ToolCallData} toolCallData - The tool call data
|
||||
* @returns {Promise<ToolCallData>} The created tool call document
|
||||
* @param {IToolCallData} toolCallData - The tool call data
|
||||
* @returns {Promise<IToolCallData>} The created tool call document
|
||||
*/
|
||||
async function createToolCall(toolCallData) {
|
||||
try {
|
||||
@@ -16,7 +18,7 @@ async function createToolCall(toolCallData) {
|
||||
/**
|
||||
* Get a tool call by ID
|
||||
* @param {string} id - The tool call document ID
|
||||
* @returns {Promise<ToolCallData|null>} The tool call document or null if not found
|
||||
* @returns {Promise<IToolCallData|null>} The tool call document or null if not found
|
||||
*/
|
||||
async function getToolCallById(id) {
|
||||
try {
|
||||
@@ -44,7 +46,7 @@ async function getToolCallsByMessage(messageId, userId) {
|
||||
* Get tool calls by conversation ID and user
|
||||
* @param {string} conversationId - The conversation ID
|
||||
* @param {string} userId - The user's ObjectId
|
||||
* @returns {Promise<ToolCallData[]>} Array of tool call documents
|
||||
* @returns {Promise<IToolCallData[]>} Array of tool call documents
|
||||
*/
|
||||
async function getToolCallsByConvo(conversationId, userId) {
|
||||
try {
|
||||
@@ -57,8 +59,8 @@ async function getToolCallsByConvo(conversationId, userId) {
|
||||
/**
|
||||
* Update a tool call
|
||||
* @param {string} id - The tool call document ID
|
||||
* @param {Partial<ToolCallData>} updateData - The data to update
|
||||
* @returns {Promise<ToolCallData|null>} The updated tool call document or null if not found
|
||||
* @param {Partial<IToolCallData>} updateData - The data to update
|
||||
* @returns {Promise<IToolCallData|null>} The updated tool call document or null if not found
|
||||
*/
|
||||
async function updateToolCall(id, updateData) {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { isEnabled } = require('~/server/utils/handleText');
|
||||
const transactionSchema = require('./schema/transaction');
|
||||
const { transactionSchema } = require('@librechat/data-schemas');
|
||||
const { getMultiplier, getCacheMultiplier } = require('./tx');
|
||||
const { logger } = require('~/config');
|
||||
const Balance = require('./Balance');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const mongoose = require('mongoose');
|
||||
const userSchema = require('~/models/schema/userSchema');
|
||||
const { userSchema } = require('@librechat/data-schemas');
|
||||
|
||||
const User = mongoose.model('User', userSchema);
|
||||
|
||||
|
||||
@@ -4,9 +4,28 @@ const { MeiliSearch } = require('meilisearch');
|
||||
const { cleanUpPrimaryKeyValue } = require('~/lib/utils/misc');
|
||||
const logger = require('~/config/meiliLogger');
|
||||
|
||||
// Environment flags
|
||||
/**
|
||||
* Flag to indicate if search is enabled based on environment variables.
|
||||
* @type {boolean}
|
||||
*/
|
||||
const searchEnabled = process.env.SEARCH && process.env.SEARCH.toLowerCase() === 'true';
|
||||
|
||||
/**
|
||||
* Flag to indicate if MeiliSearch is enabled based on required environment variables.
|
||||
* @type {boolean}
|
||||
*/
|
||||
const meiliEnabled = process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY && searchEnabled;
|
||||
|
||||
/**
|
||||
* Validates the required options for configuring the mongoMeili plugin.
|
||||
*
|
||||
* @param {Object} options - The configuration options.
|
||||
* @param {string} options.host - The MeiliSearch host.
|
||||
* @param {string} options.apiKey - The MeiliSearch API key.
|
||||
* @param {string} options.indexName - The name of the index.
|
||||
* @throws {Error} Throws an error if any required option is missing.
|
||||
*/
|
||||
const validateOptions = function (options) {
|
||||
const requiredKeys = ['host', 'apiKey', 'indexName'];
|
||||
requiredKeys.forEach((key) => {
|
||||
@@ -16,53 +35,64 @@ const validateOptions = function (options) {
|
||||
});
|
||||
};
|
||||
|
||||
// const createMeiliMongooseModel = function ({ index, indexName, client, attributesToIndex }) {
|
||||
/**
|
||||
* Factory function to create a MeiliMongooseModel class which extends a Mongoose model.
|
||||
* This class contains static and instance methods to synchronize and manage the MeiliSearch index
|
||||
* corresponding to the MongoDB collection.
|
||||
*
|
||||
* @param {Object} config - Configuration object.
|
||||
* @param {Object} config.index - The MeiliSearch index object.
|
||||
* @param {Array<string>} config.attributesToIndex - List of attributes to index.
|
||||
* @returns {Function} A class definition that will be loaded into the Mongoose schema.
|
||||
*/
|
||||
const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
|
||||
// The primary key is assumed to be the first attribute in the attributesToIndex array.
|
||||
const primaryKey = attributesToIndex[0];
|
||||
// MeiliMongooseModel is of type Mongoose.Model
|
||||
|
||||
class MeiliMongooseModel {
|
||||
/**
|
||||
* `syncWithMeili`: synchronizes the data between a MongoDB collection and a MeiliSearch index,
|
||||
* only triggered if there's ever a discrepancy determined by `api\lib\db\indexSync.js`.
|
||||
* Synchronizes the data between the MongoDB collection and the MeiliSearch index.
|
||||
*
|
||||
* 1. Fetches all documents from the MongoDB collection and the MeiliSearch index.
|
||||
* 2. Compares the documents from both sources.
|
||||
* 3. If a document exists in MeiliSearch but not in MongoDB, it's deleted from MeiliSearch.
|
||||
* 4. If a document exists in MongoDB but not in MeiliSearch, it's added to MeiliSearch.
|
||||
* 5. If a document exists in both but has different `text` or `title` fields (depending on the `primaryKey`), it's updated in MeiliSearch.
|
||||
* 6. After all operations, it updates the `_meiliIndex` field in MongoDB to indicate whether the document is indexed in MeiliSearch.
|
||||
* The synchronization process involves:
|
||||
* 1. Fetching all documents from the MongoDB collection and MeiliSearch index.
|
||||
* 2. Comparing documents from both sources.
|
||||
* 3. Deleting documents from MeiliSearch that no longer exist in MongoDB.
|
||||
* 4. Adding documents to MeiliSearch that exist in MongoDB but not in the index.
|
||||
* 5. Updating documents in MeiliSearch if key fields (such as `text` or `title`) differ.
|
||||
* 6. Updating the `_meiliIndex` field in MongoDB to indicate the indexing status.
|
||||
*
|
||||
* Note: This strategy does not use batch operations for Meilisearch as the `index.addDocuments` will discard
|
||||
* the entire batch if there's an error with one document, and will not throw an error if there's an issue.
|
||||
* Also, `index.getDocuments` needs an exact limit on the amount of documents to return, so we build the map in batches.
|
||||
* Note: The function processes documents in batches because MeiliSearch's
|
||||
* `index.getDocuments` requires an exact limit and `index.addDocuments` does not handle
|
||||
* partial failures in a batch.
|
||||
*
|
||||
* @returns {Promise} A promise that resolves when the synchronization is complete.
|
||||
*
|
||||
* @throws {Error} Throws an error if there's an issue with adding a document to MeiliSearch.
|
||||
* @returns {Promise<void>} Resolves when the synchronization is complete.
|
||||
*/
|
||||
static async syncWithMeili() {
|
||||
try {
|
||||
let moreDocuments = true;
|
||||
// Retrieve all MongoDB documents from the collection as plain JavaScript objects.
|
||||
const mongoDocuments = await this.find().lean();
|
||||
const format = (doc) => _.pick(doc, attributesToIndex);
|
||||
|
||||
// Prepare for comparison
|
||||
// Helper function to format a document by selecting only the attributes to index
|
||||
// and omitting keys starting with '$'.
|
||||
const format = (doc) =>
|
||||
_.omitBy(_.pick(doc, attributesToIndex), (v, k) => k.startsWith('$'));
|
||||
|
||||
// Build a map of MongoDB documents for quick lookup based on the primary key.
|
||||
const mongoMap = new Map(mongoDocuments.map((doc) => [doc[primaryKey], format(doc)]));
|
||||
const indexMap = new Map();
|
||||
let offset = 0;
|
||||
const batchSize = 1000;
|
||||
|
||||
// Fetch documents from the MeiliSearch index in batches.
|
||||
while (moreDocuments) {
|
||||
const batch = await index.getDocuments({ limit: batchSize, offset });
|
||||
|
||||
if (batch.results.length === 0) {
|
||||
moreDocuments = false;
|
||||
}
|
||||
|
||||
for (const doc of batch.results) {
|
||||
indexMap.set(doc[primaryKey], format(doc));
|
||||
}
|
||||
|
||||
offset += batchSize;
|
||||
}
|
||||
|
||||
@@ -70,13 +100,12 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
|
||||
|
||||
const updateOps = [];
|
||||
|
||||
// Iterate over Meili index documents
|
||||
// Process documents present in the MeiliSearch index.
|
||||
for (const [id, doc] of indexMap) {
|
||||
const update = {};
|
||||
update[primaryKey] = id;
|
||||
if (mongoMap.has(id)) {
|
||||
// Case: Update
|
||||
// If document also exists in MongoDB, would be update case
|
||||
// If document exists in MongoDB, check for discrepancies in key fields.
|
||||
if (
|
||||
(doc.text && doc.text !== mongoMap.get(id).text) ||
|
||||
(doc.title && doc.title !== mongoMap.get(id).title)
|
||||
@@ -92,8 +121,7 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
|
||||
await index.addDocuments([doc]);
|
||||
}
|
||||
} else {
|
||||
// Case: Delete
|
||||
// If document does not exist in MongoDB, its a delete case from meili index
|
||||
// If the document does not exist in MongoDB, delete it from MeiliSearch.
|
||||
await index.deleteDocument(id);
|
||||
updateOps.push({
|
||||
updateOne: { filter: update, update: { $set: { _meiliIndex: false } } },
|
||||
@@ -101,24 +129,25 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over MongoDB documents
|
||||
// Process documents present in MongoDB.
|
||||
for (const [id, doc] of mongoMap) {
|
||||
const update = {};
|
||||
update[primaryKey] = id;
|
||||
// Case: Insert
|
||||
// If document does not exist in Meili Index, Its an insert case
|
||||
// If the document is missing in the Meili index, add it.
|
||||
if (!indexMap.has(id)) {
|
||||
await index.addDocuments([doc]);
|
||||
updateOps.push({
|
||||
updateOne: { filter: update, update: { $set: { _meiliIndex: true } } },
|
||||
});
|
||||
} else if (doc._meiliIndex === false) {
|
||||
// If the document exists but is marked as not indexed, update the flag.
|
||||
updateOps.push({
|
||||
updateOne: { filter: update, update: { $set: { _meiliIndex: true } } },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Execute bulk update operations in MongoDB to update the _meiliIndex flags.
|
||||
if (updateOps.length > 0) {
|
||||
await this.collection.bulkWrite(updateOps);
|
||||
logger.debug(
|
||||
@@ -132,34 +161,47 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Set one or more settings of the meili index
|
||||
/**
|
||||
* Updates settings for the MeiliSearch index.
|
||||
*
|
||||
* @param {Object} settings - The settings to update on the MeiliSearch index.
|
||||
* @returns {Promise<Object>} Promise resolving to the update result.
|
||||
*/
|
||||
static async setMeiliIndexSettings(settings) {
|
||||
return await index.updateSettings(settings);
|
||||
}
|
||||
|
||||
// Search the index
|
||||
/**
|
||||
* Searches the MeiliSearch index and optionally populates the results with data from MongoDB.
|
||||
*
|
||||
* @param {string} q - The search query.
|
||||
* @param {Object} params - Additional search parameters for MeiliSearch.
|
||||
* @param {boolean} populate - Whether to populate search hits with full MongoDB documents.
|
||||
* @returns {Promise<Object>} The search results with populated hits if requested.
|
||||
*/
|
||||
static async meiliSearch(q, params, populate) {
|
||||
const data = await index.search(q, params);
|
||||
|
||||
// Populate hits with content from mongodb
|
||||
if (populate) {
|
||||
// Find objects into mongodb matching `objectID` from Meili search
|
||||
// Build a query using the primary key values from the search hits.
|
||||
const query = {};
|
||||
// query[primaryKey] = { $in: _.map(data.hits, primaryKey) };
|
||||
query[primaryKey] = _.map(data.hits, (hit) => cleanUpPrimaryKeyValue(hit[primaryKey]));
|
||||
// logger.debug('query', query);
|
||||
const hitsFromMongoose = await this.find(
|
||||
query,
|
||||
_.reduce(
|
||||
this.schema.obj,
|
||||
function (results, value, key) {
|
||||
return { ...results, [key]: 1 };
|
||||
},
|
||||
{ _id: 1, __v: 1 },
|
||||
),
|
||||
).lean();
|
||||
|
||||
// Add additional data from mongodb into Meili search hits
|
||||
// Build a projection object, including only keys that do not start with '$'.
|
||||
const projection = Object.keys(this.schema.obj).reduce(
|
||||
(results, key) => {
|
||||
if (!key.startsWith('$')) {
|
||||
results[key] = 1;
|
||||
}
|
||||
return results;
|
||||
},
|
||||
{ _id: 1, __v: 1 },
|
||||
);
|
||||
|
||||
// Retrieve the full documents from MongoDB.
|
||||
const hitsFromMongoose = await this.find(query, projection).lean();
|
||||
|
||||
// Merge the MongoDB documents with the search hits.
|
||||
const populatedHits = data.hits.map(function (hit) {
|
||||
const query = {};
|
||||
query[primaryKey] = hit[primaryKey];
|
||||
@@ -176,10 +218,21 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocesses the current document for indexing.
|
||||
*
|
||||
* This method:
|
||||
* - Picks only the defined attributes to index.
|
||||
* - Omits any keys starting with '$'.
|
||||
* - Replaces pipe characters ('|') in `conversationId` with '--'.
|
||||
* - Extracts and concatenates text from an array of content items.
|
||||
*
|
||||
* @returns {Object} The preprocessed object ready for indexing.
|
||||
*/
|
||||
preprocessObjectForIndex() {
|
||||
const object = _.pick(this.toJSON(), attributesToIndex);
|
||||
// NOTE: MeiliSearch does not allow | in primary key, so we replace it with - for Bing convoIds
|
||||
// object.conversationId = object.conversationId.replace(/\|/g, '-');
|
||||
const object = _.omitBy(_.pick(this.toJSON(), attributesToIndex), (v, k) =>
|
||||
k.startsWith('$'),
|
||||
);
|
||||
if (object.conversationId && object.conversationId.includes('|')) {
|
||||
object.conversationId = object.conversationId.replace(/\|/g, '--');
|
||||
}
|
||||
@@ -195,32 +248,53 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
|
||||
return object;
|
||||
}
|
||||
|
||||
// Push new document to Meili
|
||||
/**
|
||||
* Adds the current document to the MeiliSearch index.
|
||||
*
|
||||
* The method preprocesses the document, adds it to MeiliSearch, and then updates
|
||||
* the MongoDB document's `_meiliIndex` flag to true.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async addObjectToMeili() {
|
||||
const object = this.preprocessObjectForIndex();
|
||||
try {
|
||||
// logger.debug('Adding document to Meili', object);
|
||||
await index.addDocuments([object]);
|
||||
} catch (error) {
|
||||
// logger.debug('Error adding document to Meili');
|
||||
// logger.error(error);
|
||||
// Error handling can be enhanced as needed.
|
||||
logger.error('[addObjectToMeili] Error adding document to Meili', error);
|
||||
}
|
||||
|
||||
await this.collection.updateMany({ _id: this._id }, { $set: { _meiliIndex: true } });
|
||||
}
|
||||
|
||||
// Update an existing document in Meili
|
||||
/**
|
||||
* Updates the current document in the MeiliSearch index.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async updateObjectToMeili() {
|
||||
const object = _.pick(this.toJSON(), attributesToIndex);
|
||||
const object = _.omitBy(_.pick(this.toJSON(), attributesToIndex), (v, k) =>
|
||||
k.startsWith('$'),
|
||||
);
|
||||
await index.updateDocuments([object]);
|
||||
}
|
||||
|
||||
// Delete a document from Meili
|
||||
/**
|
||||
* Deletes the current document from the MeiliSearch index.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async deleteObjectFromMeili() {
|
||||
await index.deleteDocument(this._id);
|
||||
}
|
||||
|
||||
// * schema.post('save')
|
||||
/**
|
||||
* Post-save hook to synchronize the document with MeiliSearch.
|
||||
*
|
||||
* If the document is already indexed (i.e. `_meiliIndex` is true), it updates it;
|
||||
* otherwise, it adds the document to the index.
|
||||
*/
|
||||
postSaveHook() {
|
||||
if (this._meiliIndex) {
|
||||
this.updateObjectToMeili();
|
||||
@@ -229,14 +303,24 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
|
||||
}
|
||||
}
|
||||
|
||||
// * schema.post('update')
|
||||
/**
|
||||
* Post-update hook to update the document in MeiliSearch.
|
||||
*
|
||||
* This hook is triggered after a document update, ensuring that changes are
|
||||
* propagated to the MeiliSearch index if the document is indexed.
|
||||
*/
|
||||
postUpdateHook() {
|
||||
if (this._meiliIndex) {
|
||||
this.updateObjectToMeili();
|
||||
}
|
||||
}
|
||||
|
||||
// * schema.post('remove')
|
||||
/**
|
||||
* Post-remove hook to delete the document from MeiliSearch.
|
||||
*
|
||||
* This hook is triggered after a document is removed, ensuring that the document
|
||||
* is also removed from the MeiliSearch index if it was previously indexed.
|
||||
*/
|
||||
postRemoveHook() {
|
||||
if (this._meiliIndex) {
|
||||
this.deleteObjectFromMeili();
|
||||
@@ -247,11 +331,27 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
|
||||
return MeiliMongooseModel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mongoose plugin to synchronize MongoDB collections with a MeiliSearch index.
|
||||
*
|
||||
* This plugin:
|
||||
* - Validates the provided options.
|
||||
* - Adds a `_meiliIndex` field to the schema to track indexing status.
|
||||
* - Sets up a MeiliSearch client and creates an index if it doesn't already exist.
|
||||
* - Loads class methods for syncing, searching, and managing documents in MeiliSearch.
|
||||
* - Registers Mongoose hooks (post-save, post-update, post-remove, etc.) to maintain index consistency.
|
||||
*
|
||||
* @param {mongoose.Schema} schema - The Mongoose schema to which the plugin is applied.
|
||||
* @param {Object} options - Configuration options.
|
||||
* @param {string} options.host - The MeiliSearch host.
|
||||
* @param {string} options.apiKey - The MeiliSearch API key.
|
||||
* @param {string} options.indexName - The name of the MeiliSearch index.
|
||||
* @param {string} options.primaryKey - The primary key field for indexing.
|
||||
*/
|
||||
module.exports = function mongoMeili(schema, options) {
|
||||
// Vaidate Options for mongoMeili
|
||||
validateOptions(options);
|
||||
|
||||
// Add meiliIndex to schema
|
||||
// Add _meiliIndex field to the schema to track if a document has been indexed in MeiliSearch.
|
||||
schema.add({
|
||||
_meiliIndex: {
|
||||
type: Boolean,
|
||||
@@ -263,69 +363,77 @@ module.exports = function mongoMeili(schema, options) {
|
||||
|
||||
const { host, apiKey, indexName, primaryKey } = options;
|
||||
|
||||
// Setup MeiliSearch Client
|
||||
// Setup the MeiliSearch client.
|
||||
const client = new MeiliSearch({ host, apiKey });
|
||||
|
||||
// Asynchronously create the index
|
||||
// Create the index asynchronously if it doesn't exist.
|
||||
client.createIndex(indexName, { primaryKey });
|
||||
|
||||
// Setup the index to search for this schema
|
||||
// Setup the MeiliSearch index for this schema.
|
||||
const index = client.index(indexName);
|
||||
|
||||
// Collect attributes from the schema that should be indexed.
|
||||
const attributesToIndex = [
|
||||
..._.reduce(
|
||||
schema.obj,
|
||||
function (results, value, key) {
|
||||
return value.meiliIndex ? [...results, key] : results;
|
||||
// }, []), '_id'];
|
||||
},
|
||||
[],
|
||||
),
|
||||
];
|
||||
|
||||
// Load the class methods into the schema.
|
||||
schema.loadClass(createMeiliMongooseModel({ index, indexName, client, attributesToIndex }));
|
||||
|
||||
// Register hooks
|
||||
// Register Mongoose hooks to synchronize with MeiliSearch.
|
||||
|
||||
// Post-save: synchronize after a document is saved.
|
||||
schema.post('save', function (doc) {
|
||||
doc.postSaveHook();
|
||||
});
|
||||
|
||||
// Post-update: synchronize after a document is updated.
|
||||
schema.post('update', function (doc) {
|
||||
doc.postUpdateHook();
|
||||
});
|
||||
|
||||
// Post-remove: synchronize after a document is removed.
|
||||
schema.post('remove', function (doc) {
|
||||
doc.postRemoveHook();
|
||||
});
|
||||
|
||||
// Pre-deleteMany hook: remove corresponding documents from MeiliSearch when multiple documents are deleted.
|
||||
schema.pre('deleteMany', async function (next) {
|
||||
if (!meiliEnabled) {
|
||||
next();
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if the schema has a "messages" field to determine if it's a conversation schema.
|
||||
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messages')) {
|
||||
const convoIndex = client.index('convos');
|
||||
const deletedConvos = await mongoose.model('Conversation').find(this._conditions).lean();
|
||||
let promises = [];
|
||||
for (const convo of deletedConvos) {
|
||||
promises.push(convoIndex.deleteDocument(convo.conversationId));
|
||||
}
|
||||
const promises = deletedConvos.map((convo) =>
|
||||
convoIndex.deleteDocument(convo.conversationId),
|
||||
);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
// Check if the schema has a "messageId" field to determine if it's a message schema.
|
||||
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messageId')) {
|
||||
const messageIndex = client.index('messages');
|
||||
const deletedMessages = await mongoose.model('Message').find(this._conditions).lean();
|
||||
let promises = [];
|
||||
for (const message of deletedMessages) {
|
||||
promises.push(messageIndex.deleteDocument(message.messageId));
|
||||
}
|
||||
const promises = deletedMessages.map((message) =>
|
||||
messageIndex.deleteDocument(message.messageId),
|
||||
);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
return next();
|
||||
} catch (error) {
|
||||
if (meiliEnabled) {
|
||||
logger.error(
|
||||
'[MeiliMongooseModel.deleteMany] There was an issue deleting conversation indexes upon deletion, next startup may be slow due to syncing',
|
||||
'[MeiliMongooseModel.deleteMany] There was an issue deleting conversation indexes upon deletion. Next startup may be slow due to syncing.',
|
||||
error,
|
||||
);
|
||||
}
|
||||
@@ -333,17 +441,19 @@ module.exports = function mongoMeili(schema, options) {
|
||||
}
|
||||
});
|
||||
|
||||
// Post-findOneAndUpdate hook: update MeiliSearch index after a document is updated via findOneAndUpdate.
|
||||
schema.post('findOneAndUpdate', async function (doc) {
|
||||
if (!meiliEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the document is unfinished, do not update the index.
|
||||
if (doc.unfinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
let meiliDoc;
|
||||
// Doc is a Conversation
|
||||
// For conversation documents, try to fetch the document from the "convos" index.
|
||||
if (doc.messages) {
|
||||
try {
|
||||
meiliDoc = await client.index('convos').getDocument(doc.conversationId);
|
||||
@@ -356,10 +466,12 @@ module.exports = function mongoMeili(schema, options) {
|
||||
}
|
||||
}
|
||||
|
||||
// If the MeiliSearch document exists and the title is unchanged, do nothing.
|
||||
if (meiliDoc && meiliDoc.title === doc.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, trigger a post-save hook to synchronize the document.
|
||||
doc.postSaveHook();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const AuthSchema = new Schema(
|
||||
{
|
||||
authorization_type: String,
|
||||
custom_auth_header: String,
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['service_http', 'oauth', 'none'],
|
||||
},
|
||||
authorization_content_type: String,
|
||||
authorization_url: String,
|
||||
client_url: String,
|
||||
scope: String,
|
||||
token_exchange_method: {
|
||||
type: String,
|
||||
enum: ['default_post', 'basic_auth_header', null],
|
||||
},
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const actionSchema = new Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
index: true,
|
||||
required: true,
|
||||
},
|
||||
action_id: {
|
||||
type: String,
|
||||
index: true,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'action_prototype',
|
||||
},
|
||||
settings: Schema.Types.Mixed,
|
||||
agent_id: String,
|
||||
assistant_id: String,
|
||||
metadata: {
|
||||
api_key: String, // private, encrypted
|
||||
auth: AuthSchema,
|
||||
domain: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
// json_schema: Schema.Types.Mixed,
|
||||
privacy_policy_url: String,
|
||||
raw_spec: String,
|
||||
oauth_client_id: String, // private, encrypted
|
||||
oauth_client_secret: String, // private, encrypted
|
||||
},
|
||||
});
|
||||
// }, { minimize: false }); // Prevent removal of empty objects
|
||||
|
||||
module.exports = actionSchema;
|
||||
@@ -1,17 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const balanceSchema = mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
index: true,
|
||||
required: true,
|
||||
},
|
||||
// 1000 tokenCredits = 1 mill ($0.001 USD)
|
||||
tokenCredits: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = balanceSchema;
|
||||
@@ -1,19 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const categoriesSchema = new Schema({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
});
|
||||
|
||||
const categories = mongoose.model('categories', categoriesSchema);
|
||||
|
||||
module.exports = { Categories: categories };
|
||||
@@ -1,32 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const conversationTagSchema = mongoose.Schema(
|
||||
{
|
||||
tag: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
position: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
index: true,
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
conversationTagSchema.index({ tag: 1, user: 1 }, { unique: true });
|
||||
|
||||
module.exports = mongoose.model('ConversationTag', conversationTagSchema);
|
||||
@@ -1,46 +1,7 @@
|
||||
const mongoose = require('mongoose');
|
||||
const mongoMeili = require('../plugins/mongoMeili');
|
||||
const { conversationPreset } = require('./defaults');
|
||||
const convoSchema = mongoose.Schema(
|
||||
{
|
||||
conversationId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true,
|
||||
index: true,
|
||||
meiliIndex: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'New Chat',
|
||||
meiliIndex: true,
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }],
|
||||
agentOptions: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
},
|
||||
...conversationPreset,
|
||||
agent_id: {
|
||||
type: String,
|
||||
},
|
||||
tags: {
|
||||
type: [String],
|
||||
default: [],
|
||||
meiliIndex: true,
|
||||
},
|
||||
files: {
|
||||
type: [String],
|
||||
},
|
||||
expiredAt: {
|
||||
type: Date,
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
const { convoSchema } = require('@librechat/data-schemas');
|
||||
|
||||
if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||
convoSchema.plugin(mongoMeili, {
|
||||
@@ -52,10 +13,6 @@ if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||
});
|
||||
}
|
||||
|
||||
convoSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 });
|
||||
convoSchema.index({ createdAt: 1, updatedAt: 1 });
|
||||
convoSchema.index({ conversationId: 1, user: 1 }, { unique: true });
|
||||
|
||||
const Conversation = mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
|
||||
|
||||
module.exports = Conversation;
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
const { FileSources } = require('librechat-data-provider');
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* @typedef {Object} MongoFile
|
||||
* @property {ObjectId} [_id] - MongoDB Document ID
|
||||
* @property {number} [__v] - MongoDB Version Key
|
||||
* @property {ObjectId} user - User ID
|
||||
* @property {string} [conversationId] - Optional conversation ID
|
||||
* @property {string} file_id - File identifier
|
||||
* @property {string} [temp_file_id] - Temporary File identifier
|
||||
* @property {number} bytes - Size of the file in bytes
|
||||
* @property {string} filename - Name of the file
|
||||
* @property {string} filepath - Location of the file
|
||||
* @property {'file'} object - Type of object, always 'file'
|
||||
* @property {string} type - Type of file
|
||||
* @property {number} [usage=0] - Number of uses of the file
|
||||
* @property {string} [context] - Context of the file origin
|
||||
* @property {boolean} [embedded=false] - Whether or not the file is embedded in vector db
|
||||
* @property {string} [model] - The model to identify the group region of the file (for Azure OpenAI hosting)
|
||||
* @property {string} [source] - The source of the file (e.g., from FileSources)
|
||||
* @property {number} [width] - Optional width of the file
|
||||
* @property {number} [height] - Optional height of the file
|
||||
* @property {Object} [metadata] - Metadata related to the file
|
||||
* @property {string} [metadata.fileIdentifier] - Unique identifier for the file in metadata
|
||||
* @property {Date} [expiresAt] - Optional expiration date of the file
|
||||
* @property {Date} [createdAt] - Date when the file was created
|
||||
* @property {Date} [updatedAt] - Date when the file was updated
|
||||
*/
|
||||
|
||||
/** @type {MongooseSchema<MongoFile>} */
|
||||
const fileSchema = mongoose.Schema(
|
||||
{
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
index: true,
|
||||
required: true,
|
||||
},
|
||||
conversationId: {
|
||||
type: String,
|
||||
ref: 'Conversation',
|
||||
index: true,
|
||||
},
|
||||
file_id: {
|
||||
type: String,
|
||||
// required: true,
|
||||
index: true,
|
||||
},
|
||||
temp_file_id: {
|
||||
type: String,
|
||||
// required: true,
|
||||
},
|
||||
bytes: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
filename: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
filepath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
object: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'file',
|
||||
},
|
||||
embedded: {
|
||||
type: Boolean,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
context: {
|
||||
type: String,
|
||||
// required: true,
|
||||
},
|
||||
usage: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 0,
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
default: FileSources.local,
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
},
|
||||
width: Number,
|
||||
height: Number,
|
||||
metadata: {
|
||||
fileIdentifier: String,
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date,
|
||||
expires: 3600, // 1 hour in seconds
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
fileSchema.index({ createdAt: 1, updatedAt: 1 });
|
||||
|
||||
module.exports = fileSchema;
|
||||
@@ -1,145 +1,6 @@
|
||||
const mongoose = require('mongoose');
|
||||
const mongoMeili = require('~/models/plugins/mongoMeili');
|
||||
const messageSchema = mongoose.Schema(
|
||||
{
|
||||
messageId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true,
|
||||
index: true,
|
||||
meiliIndex: true,
|
||||
},
|
||||
conversationId: {
|
||||
type: String,
|
||||
index: true,
|
||||
required: true,
|
||||
meiliIndex: true,
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
index: true,
|
||||
required: true,
|
||||
default: null,
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
endpoint: {
|
||||
type: String,
|
||||
},
|
||||
conversationSignature: {
|
||||
type: String,
|
||||
},
|
||||
clientId: {
|
||||
type: String,
|
||||
},
|
||||
invocationId: {
|
||||
type: Number,
|
||||
},
|
||||
parentMessageId: {
|
||||
type: String,
|
||||
},
|
||||
tokenCount: {
|
||||
type: Number,
|
||||
},
|
||||
summaryTokenCount: {
|
||||
type: Number,
|
||||
},
|
||||
sender: {
|
||||
type: String,
|
||||
meiliIndex: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
meiliIndex: true,
|
||||
},
|
||||
summary: {
|
||||
type: String,
|
||||
},
|
||||
isCreatedByUser: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
unfinished: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
error: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
finish_reason: {
|
||||
type: String,
|
||||
},
|
||||
_meiliIndex: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
select: false,
|
||||
default: false,
|
||||
},
|
||||
files: { type: [{ type: mongoose.Schema.Types.Mixed }], default: undefined },
|
||||
plugin: {
|
||||
type: {
|
||||
latest: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
inputs: {
|
||||
type: [mongoose.Schema.Types.Mixed],
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
outputs: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
default: undefined,
|
||||
},
|
||||
plugins: { type: [{ type: mongoose.Schema.Types.Mixed }], default: undefined },
|
||||
content: {
|
||||
type: [{ type: mongoose.Schema.Types.Mixed }],
|
||||
default: undefined,
|
||||
meiliIndex: true,
|
||||
},
|
||||
thread_id: {
|
||||
type: String,
|
||||
},
|
||||
/* frontend components */
|
||||
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,
|
||||
},
|
||||
*/
|
||||
expiredAt: {
|
||||
type: Date,
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
const { messageSchema } = require('@librechat/data-schemas');
|
||||
|
||||
if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||
messageSchema.plugin(mongoMeili, {
|
||||
@@ -149,11 +10,7 @@ if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||
primaryKey: 'messageId',
|
||||
});
|
||||
}
|
||||
messageSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 });
|
||||
messageSchema.index({ createdAt: 1 });
|
||||
messageSchema.index({ messageId: 1, user: 1 }, { unique: true });
|
||||
|
||||
/** @type {mongoose.Model<TMessage>} */
|
||||
const Message = mongoose.models.Message || mongoose.model('Message', messageSchema);
|
||||
|
||||
module.exports = Message;
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const pluginAuthSchema = mongoose.Schema(
|
||||
{
|
||||
authField: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
pluginKey: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
const { pluginAuthSchema } = require('@librechat/data-schemas');
|
||||
|
||||
const PluginAuth = mongoose.models.Plugin || mongoose.model('PluginAuth', pluginAuthSchema);
|
||||
|
||||
|
||||
@@ -1,36 +1,5 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { conversationPreset } = require('./defaults');
|
||||
const presetSchema = mongoose.Schema(
|
||||
{
|
||||
presetId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'New Chat',
|
||||
meiliIndex: true,
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
defaultPreset: {
|
||||
type: Boolean,
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
},
|
||||
...conversationPreset,
|
||||
agentOptions: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
const { presetSchema } = require('@librechat/data-schemas');
|
||||
|
||||
const Preset = mongoose.models.Preset || mongoose.model('Preset', presetSchema);
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
const { Schema } = require('mongoose');
|
||||
|
||||
/**
|
||||
* @typedef {Object} MongoProject
|
||||
* @property {ObjectId} [_id] - MongoDB Document ID
|
||||
* @property {string} name - The name of the project
|
||||
* @property {ObjectId[]} promptGroupIds - Array of PromptGroup IDs associated with the project
|
||||
* @property {Date} [createdAt] - Date when the project was created (added by timestamps)
|
||||
* @property {Date} [updatedAt] - Date when the project was last updated (added by timestamps)
|
||||
*/
|
||||
|
||||
const projectSchema = new Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
promptGroupIds: {
|
||||
type: [Schema.Types.ObjectId],
|
||||
ref: 'PromptGroup',
|
||||
default: [],
|
||||
},
|
||||
agentIds: {
|
||||
type: [String],
|
||||
ref: 'Agent',
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = projectSchema;
|
||||
@@ -1,118 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { Constants } = require('librechat-data-provider');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
/**
|
||||
* @typedef {Object} MongoPromptGroup
|
||||
* @property {ObjectId} [_id] - MongoDB Document ID
|
||||
* @property {string} name - The name of the prompt group
|
||||
* @property {ObjectId} author - The author of the prompt group
|
||||
* @property {ObjectId} [projectId=null] - The project ID of the prompt group
|
||||
* @property {ObjectId} [productionId=null] - The project ID of the prompt group
|
||||
* @property {string} authorName - The name of the author of the prompt group
|
||||
* @property {number} [numberOfGenerations=0] - Number of generations the prompt group has
|
||||
* @property {string} [oneliner=''] - Oneliner description of the prompt group
|
||||
* @property {string} [category=''] - Category of the prompt group
|
||||
* @property {string} [command] - Command for the prompt group
|
||||
* @property {Date} [createdAt] - Date when the prompt group was created (added by timestamps)
|
||||
* @property {Date} [updatedAt] - Date when the prompt group was last updated (added by timestamps)
|
||||
*/
|
||||
|
||||
const promptGroupSchema = new Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
numberOfGenerations: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
oneliner: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
default: '',
|
||||
index: true,
|
||||
},
|
||||
projectIds: {
|
||||
type: [Schema.Types.ObjectId],
|
||||
ref: 'Project',
|
||||
index: true,
|
||||
},
|
||||
productionId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Prompt',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
author: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
authorName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
command: {
|
||||
type: String,
|
||||
index: true,
|
||||
validate: {
|
||||
validator: function (v) {
|
||||
return v === undefined || v === null || v === '' || /^[a-z0-9-]+$/.test(v);
|
||||
},
|
||||
message: (props) =>
|
||||
`${props.value} is not a valid command. Only lowercase alphanumeric characters and highfins (') are allowed.`,
|
||||
},
|
||||
maxlength: [
|
||||
Constants.COMMANDS_MAX_LENGTH,
|
||||
`Command cannot be longer than ${Constants.COMMANDS_MAX_LENGTH} characters`,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
const PromptGroup = mongoose.model('PromptGroup', promptGroupSchema);
|
||||
|
||||
const promptSchema = new Schema(
|
||||
{
|
||||
groupId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'PromptGroup',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
author: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
prompt: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['text', 'chat'],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
const Prompt = mongoose.model('Prompt', promptSchema);
|
||||
|
||||
promptSchema.index({ createdAt: 1, updatedAt: 1 });
|
||||
promptGroupSchema.index({ createdAt: 1, updatedAt: 1 });
|
||||
|
||||
module.exports = { Prompt, PromptGroup };
|
||||
@@ -1,20 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const sessionSchema = mongoose.Schema({
|
||||
refreshTokenHash: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
expiration: {
|
||||
type: Date,
|
||||
required: true,
|
||||
expires: 0,
|
||||
},
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = sessionSchema;
|
||||
@@ -1,54 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* @typedef {Object} ToolCallData
|
||||
* @property {string} conversationId - The ID of the conversation
|
||||
* @property {string} messageId - The ID of the message
|
||||
* @property {string} toolId - The ID of the tool
|
||||
* @property {string | ObjectId} user - The user's ObjectId
|
||||
* @property {unknown} [result] - Optional result data
|
||||
* @property {TAttachment[]} [attachments] - Optional attachments data
|
||||
* @property {number} [blockIndex] - Optional code block index
|
||||
* @property {number} [partIndex] - Optional part index
|
||||
*/
|
||||
|
||||
/** @type {MongooseSchema<ToolCallData>} */
|
||||
const toolCallSchema = mongoose.Schema(
|
||||
{
|
||||
conversationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
messageId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
toolId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
result: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
},
|
||||
attachments: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
},
|
||||
blockIndex: {
|
||||
type: Number,
|
||||
},
|
||||
partIndex: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
toolCallSchema.index({ messageId: 1, user: 1 });
|
||||
toolCallSchema.index({ conversationId: 1, user: 1 });
|
||||
|
||||
module.exports = mongoose.model('ToolCall', toolCallSchema);
|
||||
@@ -1,151 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { SystemRoles } = require('librechat-data-provider');
|
||||
|
||||
/**
|
||||
* @typedef {Object} MongoSession
|
||||
* @property {string} [refreshToken] - The refresh token
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MongoUser
|
||||
* @property {ObjectId} [_id] - MongoDB Document ID
|
||||
* @property {string} [name] - The user's name
|
||||
* @property {string} [username] - The user's username, in lowercase
|
||||
* @property {string} email - The user's email address
|
||||
* @property {boolean} emailVerified - Whether the user's email is verified
|
||||
* @property {string} [password] - The user's password, trimmed with 8-128 characters
|
||||
* @property {string} [avatar] - The URL of the user's avatar
|
||||
* @property {string} provider - The provider of the user's account (e.g., 'local', 'google')
|
||||
* @property {string} [role='USER'] - The role of the user
|
||||
* @property {string} [googleId] - Optional Google ID for the user
|
||||
* @property {string} [facebookId] - Optional Facebook ID for the user
|
||||
* @property {string} [openidId] - Optional OpenID ID for the user
|
||||
* @property {string} [ldapId] - Optional LDAP ID for the user
|
||||
* @property {string} [githubId] - Optional GitHub ID for the user
|
||||
* @property {string} [discordId] - Optional Discord ID for the user
|
||||
* @property {string} [appleId] - Optional Apple ID for the user
|
||||
* @property {Array} [plugins=[]] - List of plugins used by the user
|
||||
* @property {Array.<MongoSession>} [refreshToken] - List of sessions with refresh tokens
|
||||
* @property {Date} [expiresAt] - Optional expiration date of the file
|
||||
* @property {Date} [createdAt] - Date when the user was created (added by timestamps)
|
||||
* @property {Date} [updatedAt] - Date when the user was last updated (added by timestamps)
|
||||
*/
|
||||
|
||||
/** @type {MongooseSchema<MongoSession>} */
|
||||
const Session = mongoose.Schema({
|
||||
refreshToken: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const backupCodeSchema = mongoose.Schema({
|
||||
codeHash: { type: String, required: true },
|
||||
used: { type: Boolean, default: false },
|
||||
usedAt: { type: Date, default: null },
|
||||
});
|
||||
|
||||
/** @type {MongooseSchema<MongoUser>} */
|
||||
const userSchema = mongoose.Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
lowercase: true,
|
||||
default: '',
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: [true, 'can\'t be blank'],
|
||||
lowercase: true,
|
||||
unique: true,
|
||||
match: [/\S+@\S+\.\S+/, 'is invalid'],
|
||||
index: true,
|
||||
},
|
||||
emailVerified: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
trim: true,
|
||||
minlength: 8,
|
||||
maxlength: 128,
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
provider: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'local',
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
default: SystemRoles.USER,
|
||||
},
|
||||
googleId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true,
|
||||
},
|
||||
facebookId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true,
|
||||
},
|
||||
openidId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true,
|
||||
},
|
||||
ldapId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true,
|
||||
},
|
||||
githubId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true,
|
||||
},
|
||||
discordId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true,
|
||||
},
|
||||
appleId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true,
|
||||
},
|
||||
plugins: {
|
||||
type: Array,
|
||||
},
|
||||
totpSecret: {
|
||||
type: String,
|
||||
},
|
||||
backupCodes: {
|
||||
type: [backupCodeSchema],
|
||||
},
|
||||
refreshToken: {
|
||||
type: [Session],
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date,
|
||||
expires: 604800, // 7 days in seconds
|
||||
},
|
||||
termsAccepted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
module.exports = userSchema;
|
||||
@@ -40,14 +40,15 @@
|
||||
"@googleapis/youtube": "^20.0.0",
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@keyv/redis": "^2.8.1",
|
||||
"@langchain/community": "^0.3.14",
|
||||
"@langchain/community": "^0.3.34",
|
||||
"@langchain/core": "^0.3.40",
|
||||
"@langchain/google-genai": "^0.1.9",
|
||||
"@langchain/google-vertexai": "^0.2.0",
|
||||
"@langchain/textsplitters": "^0.1.0",
|
||||
"@librechat/agents": "^2.2.0",
|
||||
"@librechat/data-schemas": "*",
|
||||
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
||||
"axios": "1.7.8",
|
||||
"axios": "^1.8.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cohere-ai": "^7.9.1",
|
||||
"compression": "^1.7.4",
|
||||
@@ -74,7 +75,6 @@
|
||||
"keyv": "^4.5.4",
|
||||
"keyv-file": "^0.2.0",
|
||||
"klona": "^2.0.6",
|
||||
"langchain": "^0.2.19",
|
||||
"librechat-data-provider": "*",
|
||||
"librechat-mcp": "*",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -82,7 +82,7 @@
|
||||
"memorystore": "^1.6.7",
|
||||
"mime": "^3.0.0",
|
||||
"module-alias": "^2.2.3",
|
||||
"mongoose": "^8.9.5",
|
||||
"mongoose": "^8.12.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nanoid": "^3.3.7",
|
||||
"nodemailer": "^6.9.15",
|
||||
|
||||
@@ -11,17 +11,19 @@ const { encryptV2 } = require('~/server/utils/crypto');
|
||||
|
||||
const enable2FAController = async (req, res) => {
|
||||
const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, '');
|
||||
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const secret = generateTOTPSecret();
|
||||
const { plainCodes, codeObjects } = await generateBackupCodes();
|
||||
|
||||
const encryptedSecret = await encryptV2(secret);
|
||||
const user = await updateUser(userId, { totpSecret: encryptedSecret, backupCodes: codeObjects });
|
||||
// Set twoFactorEnabled to false until the user confirms 2FA.
|
||||
const user = await updateUser(userId, {
|
||||
totpSecret: encryptedSecret,
|
||||
backupCodes: codeObjects,
|
||||
twoFactorEnabled: false,
|
||||
});
|
||||
|
||||
const otpauthUrl = `otpauth://totp/${safeAppTitle}:${user.email}?secret=${secret}&issuer=${safeAppTitle}`;
|
||||
|
||||
res.status(200).json({
|
||||
otpauthUrl,
|
||||
backupCodes: plainCodes,
|
||||
@@ -37,6 +39,7 @@ const verify2FAController = async (req, res) => {
|
||||
const userId = req.user.id;
|
||||
const { token, backupCode } = req.body;
|
||||
const user = await getUserById(userId);
|
||||
// Ensure that 2FA is enabled for this user.
|
||||
if (!user || !user.totpSecret) {
|
||||
return res.status(400).json({ message: '2FA not initiated' });
|
||||
}
|
||||
@@ -52,7 +55,6 @@ const verify2FAController = async (req, res) => {
|
||||
return res.status(200).json();
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ message: 'Invalid token.' });
|
||||
} catch (err) {
|
||||
logger.error('[verify2FAController]', err);
|
||||
@@ -74,6 +76,8 @@ const confirm2FAController = async (req, res) => {
|
||||
const secret = await getTOTPSecret(user.totpSecret);
|
||||
|
||||
if (await verifyTOTP(secret, token)) {
|
||||
// Upon successful verification, enable 2FA.
|
||||
await updateUser(userId, { twoFactorEnabled: true });
|
||||
return res.status(200).json();
|
||||
}
|
||||
|
||||
@@ -87,7 +91,7 @@ const confirm2FAController = async (req, res) => {
|
||||
const disable2FAController = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
await updateUser(userId, { totpSecret: null, backupCodes: [] });
|
||||
await updateUser(userId, { totpSecret: null, backupCodes: [], twoFactorEnabled: false });
|
||||
res.status(200).json();
|
||||
} catch (err) {
|
||||
logger.error('[disable2FAController]', err);
|
||||
|
||||
@@ -10,8 +10,8 @@ const {
|
||||
ChatModelStreamHandler,
|
||||
} = require('@librechat/agents');
|
||||
const { processCodeOutput } = require('~/server/services/Files/Code/process');
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
const { saveBase64Image } = require('~/server/services/Files/process');
|
||||
const { loadAuthValues } = require('~/app/clients/tools/util');
|
||||
const { logger, sendEvent } = require('~/config');
|
||||
|
||||
/** @typedef {import('@librechat/agents').Graph} Graph */
|
||||
|
||||
@@ -223,14 +223,23 @@ class AgentClient extends BaseClient {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TMessage} message
|
||||
* @param {Array<MongoFile>} attachments
|
||||
* @returns {Promise<Array<Partial<MongoFile>>>}
|
||||
*/
|
||||
async addImageURLs(message, attachments) {
|
||||
const { files, image_urls } = await encodeAndFormat(
|
||||
const { files, text, image_urls } = await encodeAndFormat(
|
||||
this.options.req,
|
||||
attachments,
|
||||
this.options.agent.provider,
|
||||
VisionModes.agents,
|
||||
);
|
||||
message.image_urls = image_urls.length ? image_urls : undefined;
|
||||
if (text && text.length) {
|
||||
message.ocr = text;
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -308,7 +317,21 @@ class AgentClient extends BaseClient {
|
||||
assistantName: this.options?.modelLabel,
|
||||
});
|
||||
|
||||
const needsTokenCount = this.contextStrategy && !orderedMessages[i].tokenCount;
|
||||
if (message.ocr && i !== orderedMessages.length - 1) {
|
||||
if (typeof formattedMessage.content === 'string') {
|
||||
formattedMessage.content = message.ocr + '\n' + formattedMessage.content;
|
||||
} else {
|
||||
const textPart = formattedMessage.content.find((part) => part.type === 'text');
|
||||
textPart
|
||||
? (textPart.text = message.ocr + '\n' + textPart.text)
|
||||
: formattedMessage.content.unshift({ type: 'text', text: message.ocr });
|
||||
}
|
||||
} else if (message.ocr && i === orderedMessages.length - 1) {
|
||||
systemContent = [systemContent, message.ocr].join('\n');
|
||||
}
|
||||
|
||||
const needsTokenCount =
|
||||
(this.contextStrategy && !orderedMessages[i].tokenCount) || message.ocr;
|
||||
|
||||
/* If tokens were never counted, or, is a Vision request and the message has files, count again */
|
||||
if (needsTokenCount || (this.isVisionModel && (message.image_urls || message.files))) {
|
||||
@@ -812,7 +835,10 @@ class AgentClient extends BaseClient {
|
||||
'[api/server/controllers/agents/client.js #sendCompletion] Unhandled error type',
|
||||
err,
|
||||
);
|
||||
throw err;
|
||||
this.contentParts.push({
|
||||
type: ContentTypes.ERROR,
|
||||
[ContentTypes.ERROR]: `An error occurred while processing the request${err?.message ? `: ${err.message}` : ''}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const loginController = async (req, res) => {
|
||||
return res.status(400).json({ message: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
if (req.user.backupCodes != null && req.user.backupCodes.length > 0) {
|
||||
if (req.user.twoFactorEnabled) {
|
||||
const tempToken = generate2FATempToken(req.user._id);
|
||||
return res.status(200).json({ twoFAPending: true, tempToken });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { verifyTOTP, verifyBackupCode, getTOTPSecret } = require('~/server/services/twoFactorService');
|
||||
const {
|
||||
verifyTOTP,
|
||||
verifyBackupCode,
|
||||
getTOTPSecret,
|
||||
} = require('~/server/services/twoFactorService');
|
||||
const { setAuthTokens } = require('~/server/services/AuthService');
|
||||
const { getUserById } = require('~/models/userMethods');
|
||||
const { logger } = require('~/config');
|
||||
@@ -19,12 +23,12 @@ const verify2FA = async (req, res) => {
|
||||
}
|
||||
|
||||
const user = await getUserById(payload.userId);
|
||||
// Ensure that the user exists and has backup codes (i.e. 2FA enabled)
|
||||
if (!user || !(user.backupCodes && user.backupCodes.length > 0)) {
|
||||
// Ensure that the user exists and has 2FA enabled
|
||||
if (!user || !user.twoFactorEnabled) {
|
||||
return res.status(400).json({ message: '2FA is not enabled for this user' });
|
||||
}
|
||||
|
||||
// Use the new getTOTPSecret function to retrieve (and decrypt if necessary) the TOTP secret.
|
||||
// Retrieve (and decrypt if necessary) the TOTP secret.
|
||||
const secret = await getTOTPSecret(user.totpSecret);
|
||||
|
||||
let verified = false;
|
||||
@@ -39,9 +43,7 @@ const verify2FA = async (req, res) => {
|
||||
}
|
||||
|
||||
// Prepare user data for response.
|
||||
// If the user is a plain object (from lean queries), we create a shallow copy.
|
||||
const userData = user.toObject ? user.toObject() : { ...user };
|
||||
// Remove sensitive fields.
|
||||
delete userData.password;
|
||||
delete userData.__v;
|
||||
delete userData.totpSecret;
|
||||
|
||||
@@ -10,7 +10,8 @@ const {
|
||||
const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process');
|
||||
const { processCodeOutput } = require('~/server/services/Files/Code/process');
|
||||
const { createToolCall, getToolCallsByConvo } = require('~/models/ToolCall');
|
||||
const { loadAuthValues, loadTools } = require('~/app/clients/tools/util');
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
const { loadTools } = require('~/app/clients/tools/util');
|
||||
const { checkAccess } = require('~/server/middleware');
|
||||
const { getMessage } = require('~/models/Message');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
@@ -120,7 +120,7 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
|
||||
{ promptTokens, completionTokens },
|
||||
);
|
||||
|
||||
saveMessage(
|
||||
await saveMessage(
|
||||
req,
|
||||
{ ...responseMessage, user },
|
||||
{ context: 'api/server/middleware/abortMiddleware.js' },
|
||||
|
||||
@@ -10,7 +10,6 @@ const openAI = require('~/server/services/Endpoints/openAI');
|
||||
const agents = require('~/server/services/Endpoints/agents');
|
||||
const custom = require('~/server/services/Endpoints/custom');
|
||||
const google = require('~/server/services/Endpoints/google');
|
||||
const { getConvoFiles } = require('~/models/Conversation');
|
||||
const { handleError } = require('~/server/utils');
|
||||
|
||||
const buildFunction = {
|
||||
@@ -87,16 +86,8 @@ async function buildEndpointOption(req, res, next) {
|
||||
|
||||
// TODO: use `getModelsConfig` only when necessary
|
||||
const modelsConfig = await getModelsConfig(req);
|
||||
const { resendFiles = true } = req.body.endpointOption;
|
||||
req.body.endpointOption.modelsConfig = modelsConfig;
|
||||
if (isAgents && resendFiles && req.body.conversationId) {
|
||||
const fileIds = await getConvoFiles(req.body.conversationId);
|
||||
const requestFiles = req.body.files ?? [];
|
||||
if (requestFiles.length || fileIds.length) {
|
||||
req.body.endpointOption.attachments = processFiles(requestFiles, fileIds);
|
||||
}
|
||||
} else if (req.body.files) {
|
||||
// hold the promise
|
||||
if (req.body.files && !isAgents) {
|
||||
req.body.endpointOption.attachments = processFiles(req.body.files);
|
||||
}
|
||||
next();
|
||||
|
||||
@@ -16,7 +16,7 @@ const {
|
||||
} = 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 { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
const { getAgent } = require('~/models/Agent');
|
||||
const { getFiles } = require('~/models/File');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
const { FileSources, EModelEndpoint, getConfigDefaults } = require('librechat-data-provider');
|
||||
const {
|
||||
FileSources,
|
||||
EModelEndpoint,
|
||||
loadOCRConfig,
|
||||
getConfigDefaults,
|
||||
} = require('librechat-data-provider');
|
||||
const { checkVariables, checkHealth, checkConfig, checkAzureVariables } = require('./start/checks');
|
||||
const { azureAssistantsDefaults, assistantsConfigSetup } = require('./start/assistants');
|
||||
const { initializeFirebase } = require('./Files/Firebase/initialize');
|
||||
@@ -25,6 +30,7 @@ const AppService = async (app) => {
|
||||
const config = (await loadCustomConfig()) ?? {};
|
||||
const configDefaults = getConfigDefaults();
|
||||
|
||||
const ocr = loadOCRConfig(config.ocr);
|
||||
const filteredTools = config.filteredTools;
|
||||
const includedTools = config.includedTools;
|
||||
const fileStrategy = config.fileStrategy ?? configDefaults.fileStrategy;
|
||||
@@ -57,6 +63,7 @@ const AppService = async (app) => {
|
||||
const interfaceConfig = await loadDefaultInterface(config, configDefaults);
|
||||
|
||||
const defaultLocals = {
|
||||
ocr,
|
||||
paths,
|
||||
fileStrategy,
|
||||
socialLogins,
|
||||
|
||||
@@ -120,6 +120,7 @@ describe('AppService', () => {
|
||||
},
|
||||
},
|
||||
paths: expect.anything(),
|
||||
ocr: expect.anything(),
|
||||
imageOutputType: expect.any(String),
|
||||
fileConfig: undefined,
|
||||
secureImageLinks: undefined,
|
||||
@@ -588,4 +589,33 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not parse environment variable references in OCR config', async () => {
|
||||
// Mock custom configuration with env variable references in OCR config
|
||||
const mockConfig = {
|
||||
ocr: {
|
||||
apiKey: '${OCR_API_KEY_CUSTOM_VAR_NAME}',
|
||||
baseURL: '${OCR_BASEURL_CUSTOM_VAR_NAME}',
|
||||
strategy: 'mistral_ocr',
|
||||
mistralModel: 'mistral-medium',
|
||||
},
|
||||
};
|
||||
|
||||
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(mockConfig));
|
||||
|
||||
// Set actual environment variables with different values
|
||||
process.env.OCR_API_KEY_CUSTOM_VAR_NAME = 'actual-api-key';
|
||||
process.env.OCR_BASEURL_CUSTOM_VAR_NAME = 'https://actual-ocr-url.com';
|
||||
|
||||
// Initialize app
|
||||
const app = { locals: {} };
|
||||
await AppService(app);
|
||||
|
||||
// Verify that the raw string references were preserved and not interpolated
|
||||
expect(app.locals.ocr).toBeDefined();
|
||||
expect(app.locals.ocr.apiKey).toEqual('${OCR_API_KEY_CUSTOM_VAR_NAME}');
|
||||
expect(app.locals.ocr.baseURL).toEqual('${OCR_BASEURL_CUSTOM_VAR_NAME}');
|
||||
expect(app.locals.ocr.strategy).toEqual('mistral_ocr');
|
||||
expect(app.locals.ocr.mistralModel).toEqual('mistral-medium');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,15 +2,8 @@ const { loadAgent } = require('~/models/Agent');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const buildOptions = (req, endpoint, parsedBody) => {
|
||||
const {
|
||||
spec,
|
||||
iconURL,
|
||||
agent_id,
|
||||
instructions,
|
||||
maxContextTokens,
|
||||
resendFiles = true,
|
||||
...model_parameters
|
||||
} = parsedBody;
|
||||
const { spec, iconURL, agent_id, instructions, maxContextTokens, ...model_parameters } =
|
||||
parsedBody;
|
||||
const agentPromise = loadAgent({
|
||||
req,
|
||||
agent_id,
|
||||
@@ -24,7 +17,6 @@ const buildOptions = (req, endpoint, parsedBody) => {
|
||||
iconURL,
|
||||
endpoint,
|
||||
agent_id,
|
||||
resendFiles,
|
||||
instructions,
|
||||
maxContextTokens,
|
||||
model_parameters,
|
||||
|
||||
@@ -2,6 +2,7 @@ const { createContentAggregator, Providers } = require('@librechat/agents');
|
||||
const {
|
||||
EModelEndpoint,
|
||||
getResponseSender,
|
||||
AgentCapabilities,
|
||||
providerEndpointMap,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
@@ -15,10 +16,13 @@ const initCustom = require('~/server/services/Endpoints/custom/initialize');
|
||||
const initGoogle = require('~/server/services/Endpoints/google/initialize');
|
||||
const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
|
||||
const { getCustomEndpointConfig } = require('~/server/services/Config');
|
||||
const { processFiles } = require('~/server/services/Files/process');
|
||||
const { loadAgentTools } = require('~/server/services/ToolService');
|
||||
const AgentClient = require('~/server/controllers/agents/client');
|
||||
const { getToolFiles } = require('~/models/Conversation');
|
||||
const { getModelMaxTokens } = require('~/utils');
|
||||
const { getAgent } = require('~/models/Agent');
|
||||
const { getFiles } = require('~/models/File');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const providerConfigMap = {
|
||||
@@ -34,20 +38,38 @@ const providerConfigMap = {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ServerRequest} req
|
||||
* @param {Promise<Array<MongoFile | null>> | undefined} _attachments
|
||||
* @param {AgentToolResources | undefined} _tool_resources
|
||||
* @returns {Promise<{ attachments: Array<MongoFile | undefined> | undefined, tool_resources: AgentToolResources | undefined }>}
|
||||
*/
|
||||
const primeResources = async (_attachments, _tool_resources) => {
|
||||
const primeResources = async (req, _attachments, _tool_resources) => {
|
||||
try {
|
||||
/** @type {Array<MongoFile | undefined> | undefined} */
|
||||
let attachments;
|
||||
const tool_resources = _tool_resources ?? {};
|
||||
const isOCREnabled = (req.app.locals?.[EModelEndpoint.agents]?.capabilities ?? []).includes(
|
||||
AgentCapabilities.ocr,
|
||||
);
|
||||
if (tool_resources.ocr?.file_ids && isOCREnabled) {
|
||||
const context = await getFiles(
|
||||
{
|
||||
file_id: { $in: tool_resources.ocr.file_ids },
|
||||
},
|
||||
{},
|
||||
{},
|
||||
);
|
||||
attachments = (attachments ?? []).concat(context);
|
||||
}
|
||||
if (!_attachments) {
|
||||
return { attachments: undefined, tool_resources: _tool_resources };
|
||||
return { attachments, tool_resources };
|
||||
}
|
||||
/** @type {Array<MongoFile | undefined> | undefined} */
|
||||
const files = await _attachments;
|
||||
const attachments = [];
|
||||
const tool_resources = _tool_resources ?? {};
|
||||
if (!attachments) {
|
||||
/** @type {Array<MongoFile | undefined>} */
|
||||
attachments = [];
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
if (!file) {
|
||||
@@ -82,7 +104,6 @@ const primeResources = async (_attachments, _tool_resources) => {
|
||||
* @param {ServerResponse} params.res
|
||||
* @param {Agent} params.agent
|
||||
* @param {object} [params.endpointOption]
|
||||
* @param {AgentToolResources} [params.tool_resources]
|
||||
* @param {boolean} [params.isInitialAgent]
|
||||
* @returns {Promise<Agent>}
|
||||
*/
|
||||
@@ -91,9 +112,28 @@ const initializeAgentOptions = async ({
|
||||
res,
|
||||
agent,
|
||||
endpointOption,
|
||||
tool_resources,
|
||||
isInitialAgent = false,
|
||||
}) => {
|
||||
let currentFiles;
|
||||
const requestFiles = req.body.files ?? [];
|
||||
if (
|
||||
isInitialAgent &&
|
||||
req.body.conversationId != null &&
|
||||
agent.model_parameters?.resendFiles === true
|
||||
) {
|
||||
const fileIds = (await getToolFiles(req.body.conversationId)).map((f) => f.file_id);
|
||||
if (requestFiles.length || fileIds.length) {
|
||||
currentFiles = await processFiles(requestFiles, fileIds);
|
||||
}
|
||||
} else if (isInitialAgent && requestFiles.length) {
|
||||
currentFiles = await processFiles(requestFiles);
|
||||
}
|
||||
|
||||
const { attachments, tool_resources } = await primeResources(
|
||||
req,
|
||||
currentFiles,
|
||||
agent.tool_resources,
|
||||
);
|
||||
const { tools, toolContextMap } = await loadAgentTools({
|
||||
req,
|
||||
res,
|
||||
@@ -160,6 +200,7 @@ const initializeAgentOptions = async ({
|
||||
return {
|
||||
...agent,
|
||||
tools,
|
||||
attachments,
|
||||
toolContextMap,
|
||||
maxContextTokens:
|
||||
agent.max_context_tokens ??
|
||||
@@ -197,11 +238,6 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
throw new Error('Agent not found');
|
||||
}
|
||||
|
||||
const { attachments, tool_resources } = await primeResources(
|
||||
endpointOption.attachments,
|
||||
primaryAgent.tool_resources,
|
||||
);
|
||||
|
||||
const agentConfigs = new Map();
|
||||
|
||||
// Handle primary agent
|
||||
@@ -210,7 +246,6 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
res,
|
||||
agent: primaryAgent,
|
||||
endpointOption,
|
||||
tool_resources,
|
||||
isInitialAgent: true,
|
||||
});
|
||||
|
||||
@@ -240,18 +275,19 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
|
||||
const client = new AgentClient({
|
||||
req,
|
||||
agent: primaryConfig,
|
||||
sender,
|
||||
attachments,
|
||||
contentParts,
|
||||
agentConfigs,
|
||||
eventHandlers,
|
||||
collectedUsage,
|
||||
artifactPromises,
|
||||
agent: primaryConfig,
|
||||
spec: endpointOption.spec,
|
||||
iconURL: endpointOption.iconURL,
|
||||
agentConfigs,
|
||||
endpoint: EModelEndpoint.agents,
|
||||
attachments: primaryConfig.attachments,
|
||||
maxContextTokens: primaryConfig.maxContextTokens,
|
||||
resendFiles: primaryConfig.model_parameters?.resendFiles ?? true,
|
||||
});
|
||||
|
||||
return { client };
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
const axios = require('axios');
|
||||
const FormData = require('form-data');
|
||||
const { getCodeBaseURL } = require('@librechat/agents');
|
||||
const { createAxiosInstance } = require('~/config');
|
||||
const { logAxiosError } = require('~/utils');
|
||||
|
||||
const axios = createAxiosInstance();
|
||||
|
||||
const MAX_FILE_SIZE = 150 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
@@ -27,13 +29,6 @@ async function getCodeOutputDownloadStream(fileIdentifier, apiKey) {
|
||||
timeout: 15000,
|
||||
};
|
||||
|
||||
if (process.env.PROXY) {
|
||||
options.proxy = {
|
||||
host: process.env.PROXY,
|
||||
protocol: process.env.PROXY.startsWith('https') ? 'https' : 'http',
|
||||
};
|
||||
}
|
||||
|
||||
const response = await axios(options);
|
||||
return response;
|
||||
} catch (error) {
|
||||
@@ -79,13 +74,6 @@ async function uploadCodeEnvFile({ req, stream, filename, apiKey, entity_id = ''
|
||||
maxBodyLength: MAX_FILE_SIZE,
|
||||
};
|
||||
|
||||
if (process.env.PROXY) {
|
||||
options.proxy = {
|
||||
host: process.env.PROXY,
|
||||
protocol: process.env.PROXY.startsWith('https') ? 'https' : 'http',
|
||||
};
|
||||
}
|
||||
|
||||
const response = await axios.post(`${baseURL}/upload`, form, options);
|
||||
|
||||
/** @type {{ message: string; session_id: string; files: Array<{ fileId: string; filename: string }> }} */
|
||||
|
||||
207
api/server/services/Files/MistralOCR/crud.js
Normal file
207
api/server/services/Files/MistralOCR/crud.js
Normal file
@@ -0,0 +1,207 @@
|
||||
// ~/server/services/Files/MistralOCR/crud.js
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const FormData = require('form-data');
|
||||
const { FileSources, envVarRegex, extractEnvVariable } = require('librechat-data-provider');
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
const { logger, createAxiosInstance } = require('~/config');
|
||||
const { logAxiosError } = require('~/utils');
|
||||
|
||||
const axios = createAxiosInstance();
|
||||
|
||||
/**
|
||||
* Uploads a document to Mistral API using file streaming to avoid loading the entire file into memory
|
||||
*
|
||||
* @param {Object} params Upload parameters
|
||||
* @param {string} params.filePath The path to the file on disk
|
||||
* @param {string} [params.fileName] Optional filename to use (defaults to the name from filePath)
|
||||
* @param {string} params.apiKey Mistral API key
|
||||
* @param {string} [params.baseURL=https://api.mistral.ai/v1] Mistral API base URL
|
||||
* @returns {Promise<Object>} The response from Mistral API
|
||||
*/
|
||||
async function uploadDocumentToMistral({
|
||||
filePath,
|
||||
fileName = '',
|
||||
apiKey,
|
||||
baseURL = 'https://api.mistral.ai/v1',
|
||||
}) {
|
||||
const form = new FormData();
|
||||
form.append('purpose', 'ocr');
|
||||
const actualFileName = fileName || path.basename(filePath);
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
form.append('file', fileStream, { filename: actualFileName });
|
||||
|
||||
return axios
|
||||
.post(`${baseURL}/files`, form, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
...form.getHeaders(),
|
||||
},
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Infinity,
|
||||
})
|
||||
.then((res) => res.data)
|
||||
.catch((error) => {
|
||||
logger.error('Error uploading document to Mistral:', error.message);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
async function getSignedUrl({
|
||||
apiKey,
|
||||
fileId,
|
||||
expiry = 24,
|
||||
baseURL = 'https://api.mistral.ai/v1',
|
||||
}) {
|
||||
return axios
|
||||
.get(`${baseURL}/files/${fileId}/url?expiry=${expiry}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
})
|
||||
.then((res) => res.data)
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching signed URL:', error.message);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {string} params.apiKey
|
||||
* @param {string} params.documentUrl
|
||||
* @param {string} [params.baseURL]
|
||||
* @returns {Promise<OCRResult>}
|
||||
*/
|
||||
async function performOCR({
|
||||
apiKey,
|
||||
documentUrl,
|
||||
model = 'mistral-ocr-latest',
|
||||
baseURL = 'https://api.mistral.ai/v1',
|
||||
}) {
|
||||
return axios
|
||||
.post(
|
||||
`${baseURL}/ocr`,
|
||||
{
|
||||
model,
|
||||
include_image_base64: false,
|
||||
document: {
|
||||
type: 'document_url',
|
||||
document_url: documentUrl,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((res) => res.data)
|
||||
.catch((error) => {
|
||||
logger.error('Error performing OCR:', error.message);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function extractVariableName(str) {
|
||||
const match = str.match(envVarRegex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
const uploadMistralOCR = async ({ req, file, file_id, entity_id }) => {
|
||||
try {
|
||||
/** @type {TCustomConfig['ocr']} */
|
||||
const ocrConfig = req.app.locals?.ocr;
|
||||
|
||||
const apiKeyConfig = ocrConfig.apiKey || '';
|
||||
const baseURLConfig = ocrConfig.baseURL || '';
|
||||
|
||||
const isApiKeyEnvVar = envVarRegex.test(apiKeyConfig);
|
||||
const isBaseURLEnvVar = envVarRegex.test(baseURLConfig);
|
||||
|
||||
const isApiKeyEmpty = !apiKeyConfig.trim();
|
||||
const isBaseURLEmpty = !baseURLConfig.trim();
|
||||
|
||||
let apiKey, baseURL;
|
||||
|
||||
if (isApiKeyEnvVar || isBaseURLEnvVar || isApiKeyEmpty || isBaseURLEmpty) {
|
||||
const apiKeyVarName = isApiKeyEnvVar ? extractVariableName(apiKeyConfig) : 'OCR_API_KEY';
|
||||
const baseURLVarName = isBaseURLEnvVar ? extractVariableName(baseURLConfig) : 'OCR_BASEURL';
|
||||
|
||||
const authValues = await loadAuthValues({
|
||||
userId: req.user.id,
|
||||
authFields: [baseURLVarName, apiKeyVarName],
|
||||
optional: new Set([baseURLVarName]),
|
||||
});
|
||||
|
||||
apiKey = authValues[apiKeyVarName];
|
||||
baseURL = authValues[baseURLVarName];
|
||||
} else {
|
||||
apiKey = apiKeyConfig;
|
||||
baseURL = baseURLConfig;
|
||||
}
|
||||
|
||||
const mistralFile = await uploadDocumentToMistral({
|
||||
filePath: file.path,
|
||||
fileName: file.originalname,
|
||||
apiKey,
|
||||
baseURL,
|
||||
});
|
||||
|
||||
const modelConfig = ocrConfig.mistralModel || '';
|
||||
const model = envVarRegex.test(modelConfig)
|
||||
? extractEnvVariable(modelConfig)
|
||||
: modelConfig.trim() || 'mistral-ocr-latest';
|
||||
|
||||
const signedUrlResponse = await getSignedUrl({
|
||||
apiKey,
|
||||
baseURL,
|
||||
fileId: mistralFile.id,
|
||||
});
|
||||
|
||||
const ocrResult = await performOCR({
|
||||
apiKey,
|
||||
baseURL,
|
||||
model,
|
||||
documentUrl: signedUrlResponse.url,
|
||||
});
|
||||
|
||||
let aggregatedText = '';
|
||||
const images = [];
|
||||
ocrResult.pages.forEach((page, index) => {
|
||||
if (ocrResult.pages.length > 1) {
|
||||
aggregatedText += `# PAGE ${index + 1}\n`;
|
||||
}
|
||||
|
||||
aggregatedText += page.markdown + '\n\n';
|
||||
|
||||
if (page.images && page.images.length > 0) {
|
||||
page.images.forEach((image) => {
|
||||
if (image.image_base64) {
|
||||
images.push(image.image_base64);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
filename: file.originalname,
|
||||
bytes: aggregatedText.length * 4,
|
||||
filepath: FileSources.mistral_ocr,
|
||||
text: aggregatedText,
|
||||
images,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = 'Error uploading document to Mistral OCR API';
|
||||
logAxiosError({ error, message });
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
uploadDocumentToMistral,
|
||||
uploadMistralOCR,
|
||||
getSignedUrl,
|
||||
performOCR,
|
||||
};
|
||||
737
api/server/services/Files/MistralOCR/crud.spec.js
Normal file
737
api/server/services/Files/MistralOCR/crud.spec.js
Normal file
@@ -0,0 +1,737 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const mockAxios = {
|
||||
interceptors: {
|
||||
request: { use: jest.fn(), eject: jest.fn() },
|
||||
response: { use: jest.fn(), eject: jest.fn() },
|
||||
},
|
||||
create: jest.fn().mockReturnValue({
|
||||
defaults: {
|
||||
proxy: null,
|
||||
},
|
||||
get: jest.fn().mockResolvedValue({ data: {} }),
|
||||
post: jest.fn().mockResolvedValue({ data: {} }),
|
||||
put: jest.fn().mockResolvedValue({ data: {} }),
|
||||
delete: jest.fn().mockResolvedValue({ data: {} }),
|
||||
}),
|
||||
get: jest.fn().mockResolvedValue({ data: {} }),
|
||||
post: jest.fn().mockResolvedValue({ data: {} }),
|
||||
put: jest.fn().mockResolvedValue({ data: {} }),
|
||||
delete: jest.fn().mockResolvedValue({ data: {} }),
|
||||
reset: jest.fn().mockImplementation(function () {
|
||||
this.get.mockClear();
|
||||
this.post.mockClear();
|
||||
this.put.mockClear();
|
||||
this.delete.mockClear();
|
||||
this.create.mockClear();
|
||||
}),
|
||||
};
|
||||
|
||||
jest.mock('axios', () => mockAxios);
|
||||
jest.mock('fs');
|
||||
jest.mock('~/utils', () => ({
|
||||
logAxiosError: jest.fn(),
|
||||
}));
|
||||
jest.mock('~/config', () => ({
|
||||
logger: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
createAxiosInstance: () => mockAxios,
|
||||
}));
|
||||
jest.mock('~/server/services/Tools/credentials', () => ({
|
||||
loadAuthValues: jest.fn(),
|
||||
}));
|
||||
|
||||
const { uploadDocumentToMistral, uploadMistralOCR, getSignedUrl, performOCR } = require('./crud');
|
||||
|
||||
describe('MistralOCR Service', () => {
|
||||
afterEach(() => {
|
||||
mockAxios.reset();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('uploadDocumentToMistral', () => {
|
||||
beforeEach(() => {
|
||||
// Create a more complete mock for file streams that FormData can work with
|
||||
const mockReadStream = {
|
||||
on: jest.fn().mockImplementation(function (event, handler) {
|
||||
// Simulate immediate 'end' event to make FormData complete processing
|
||||
if (event === 'end') {
|
||||
handler();
|
||||
}
|
||||
return this;
|
||||
}),
|
||||
pipe: jest.fn().mockImplementation(function () {
|
||||
return this;
|
||||
}),
|
||||
pause: jest.fn(),
|
||||
resume: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
once: jest.fn(),
|
||||
destroy: jest.fn(),
|
||||
};
|
||||
|
||||
fs.createReadStream = jest.fn().mockReturnValue(mockReadStream);
|
||||
|
||||
// Mock FormData's append to avoid actual stream processing
|
||||
jest.mock('form-data', () => {
|
||||
const mockFormData = function () {
|
||||
return {
|
||||
append: jest.fn(),
|
||||
getHeaders: jest
|
||||
.fn()
|
||||
.mockReturnValue({ 'content-type': 'multipart/form-data; boundary=---boundary' }),
|
||||
getBuffer: jest.fn().mockReturnValue(Buffer.from('mock-form-data')),
|
||||
getLength: jest.fn().mockReturnValue(100),
|
||||
};
|
||||
};
|
||||
return mockFormData;
|
||||
});
|
||||
});
|
||||
|
||||
it('should upload a document to Mistral API using file streaming', async () => {
|
||||
const mockResponse = { data: { id: 'file-123', purpose: 'ocr' } };
|
||||
mockAxios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await uploadDocumentToMistral({
|
||||
filePath: '/path/to/test.pdf',
|
||||
fileName: 'test.pdf',
|
||||
apiKey: 'test-api-key',
|
||||
});
|
||||
|
||||
// Check that createReadStream was called with the correct file path
|
||||
expect(fs.createReadStream).toHaveBeenCalledWith('/path/to/test.pdf');
|
||||
|
||||
// Since we're mocking FormData, we'll just check that axios was called correctly
|
||||
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||
'https://api.mistral.ai/v1/files',
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer test-api-key',
|
||||
}),
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Infinity,
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('should handle errors during document upload', async () => {
|
||||
const errorMessage = 'API error';
|
||||
mockAxios.post.mockRejectedValueOnce(new Error(errorMessage));
|
||||
|
||||
await expect(
|
||||
uploadDocumentToMistral({
|
||||
filePath: '/path/to/test.pdf',
|
||||
fileName: 'test.pdf',
|
||||
apiKey: 'test-api-key',
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
|
||||
const { logger } = require('~/config');
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Error uploading document to Mistral:'),
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSignedUrl', () => {
|
||||
it('should fetch signed URL from Mistral API', async () => {
|
||||
const mockResponse = { data: { url: 'https://document-url.com' } };
|
||||
mockAxios.get.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await getSignedUrl({
|
||||
fileId: 'file-123',
|
||||
apiKey: 'test-api-key',
|
||||
});
|
||||
|
||||
expect(mockAxios.get).toHaveBeenCalledWith(
|
||||
'https://api.mistral.ai/v1/files/file-123/url?expiry=24',
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer test-api-key',
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('should handle errors when fetching signed URL', async () => {
|
||||
const errorMessage = 'API error';
|
||||
mockAxios.get.mockRejectedValueOnce(new Error(errorMessage));
|
||||
|
||||
await expect(
|
||||
getSignedUrl({
|
||||
fileId: 'file-123',
|
||||
apiKey: 'test-api-key',
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
|
||||
const { logger } = require('~/config');
|
||||
expect(logger.error).toHaveBeenCalledWith('Error fetching signed URL:', errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('performOCR', () => {
|
||||
it('should perform OCR using Mistral API', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
pages: [{ markdown: 'Page 1 content' }, { markdown: 'Page 2 content' }],
|
||||
},
|
||||
};
|
||||
mockAxios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await performOCR({
|
||||
apiKey: 'test-api-key',
|
||||
documentUrl: 'https://document-url.com',
|
||||
model: 'mistral-ocr-latest',
|
||||
});
|
||||
|
||||
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||
'https://api.mistral.ai/v1/ocr',
|
||||
{
|
||||
model: 'mistral-ocr-latest',
|
||||
include_image_base64: false,
|
||||
document: {
|
||||
type: 'document_url',
|
||||
document_url: 'https://document-url.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer test-api-key',
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('should handle errors during OCR processing', async () => {
|
||||
const errorMessage = 'OCR processing error';
|
||||
mockAxios.post.mockRejectedValueOnce(new Error(errorMessage));
|
||||
|
||||
await expect(
|
||||
performOCR({
|
||||
apiKey: 'test-api-key',
|
||||
documentUrl: 'https://document-url.com',
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
|
||||
const { logger } = require('~/config');
|
||||
expect(logger.error).toHaveBeenCalledWith('Error performing OCR:', errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadMistralOCR', () => {
|
||||
beforeEach(() => {
|
||||
const mockReadStream = {
|
||||
on: jest.fn().mockImplementation(function (event, handler) {
|
||||
if (event === 'end') {
|
||||
handler();
|
||||
}
|
||||
return this;
|
||||
}),
|
||||
pipe: jest.fn().mockImplementation(function () {
|
||||
return this;
|
||||
}),
|
||||
pause: jest.fn(),
|
||||
resume: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
once: jest.fn(),
|
||||
destroy: jest.fn(),
|
||||
};
|
||||
|
||||
fs.createReadStream = jest.fn().mockReturnValue(mockReadStream);
|
||||
});
|
||||
|
||||
it('should process OCR for a file with standard configuration', async () => {
|
||||
// Setup mocks
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
loadAuthValues.mockResolvedValue({
|
||||
OCR_API_KEY: 'test-api-key',
|
||||
OCR_BASEURL: 'https://api.mistral.ai/v1',
|
||||
});
|
||||
|
||||
// Mock file upload response
|
||||
mockAxios.post.mockResolvedValueOnce({
|
||||
data: { id: 'file-123', purpose: 'ocr' },
|
||||
});
|
||||
|
||||
// Mock signed URL response
|
||||
mockAxios.get.mockResolvedValueOnce({
|
||||
data: { url: 'https://signed-url.com' },
|
||||
});
|
||||
|
||||
// Mock OCR response with text and images
|
||||
mockAxios.post.mockResolvedValueOnce({
|
||||
data: {
|
||||
pages: [
|
||||
{
|
||||
markdown: 'Page 1 content',
|
||||
images: [{ image_base64: 'base64image1' }],
|
||||
},
|
||||
{
|
||||
markdown: 'Page 2 content',
|
||||
images: [{ image_base64: 'base64image2' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const req = {
|
||||
user: { id: 'user123' },
|
||||
app: {
|
||||
locals: {
|
||||
ocr: {
|
||||
// Use environment variable syntax to ensure loadAuthValues is called
|
||||
apiKey: '${OCR_API_KEY}',
|
||||
baseURL: '${OCR_BASEURL}',
|
||||
mistralModel: 'mistral-medium',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const file = {
|
||||
path: '/tmp/upload/file.pdf',
|
||||
originalname: 'document.pdf',
|
||||
};
|
||||
|
||||
const result = await uploadMistralOCR({
|
||||
req,
|
||||
file,
|
||||
file_id: 'file123',
|
||||
entity_id: 'entity123',
|
||||
});
|
||||
|
||||
expect(fs.createReadStream).toHaveBeenCalledWith('/tmp/upload/file.pdf');
|
||||
|
||||
expect(loadAuthValues).toHaveBeenCalledWith({
|
||||
userId: 'user123',
|
||||
authFields: ['OCR_BASEURL', 'OCR_API_KEY'],
|
||||
optional: expect.any(Set),
|
||||
});
|
||||
|
||||
// Verify OCR result
|
||||
expect(result).toEqual({
|
||||
filename: 'document.pdf',
|
||||
bytes: expect.any(Number),
|
||||
filepath: 'mistral_ocr',
|
||||
text: expect.stringContaining('# PAGE 1'),
|
||||
images: ['base64image1', 'base64image2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should process variable references in configuration', async () => {
|
||||
// Setup mocks with environment variables
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
loadAuthValues.mockResolvedValue({
|
||||
CUSTOM_API_KEY: 'custom-api-key',
|
||||
CUSTOM_BASEURL: 'https://custom-api.mistral.ai/v1',
|
||||
});
|
||||
|
||||
// Mock API responses
|
||||
mockAxios.post.mockResolvedValueOnce({
|
||||
data: { id: 'file-123', purpose: 'ocr' },
|
||||
});
|
||||
mockAxios.get.mockResolvedValueOnce({
|
||||
data: { url: 'https://signed-url.com' },
|
||||
});
|
||||
mockAxios.post.mockResolvedValueOnce({
|
||||
data: {
|
||||
pages: [{ markdown: 'Content from custom API' }],
|
||||
},
|
||||
});
|
||||
|
||||
const req = {
|
||||
user: { id: 'user123' },
|
||||
app: {
|
||||
locals: {
|
||||
ocr: {
|
||||
apiKey: '${CUSTOM_API_KEY}',
|
||||
baseURL: '${CUSTOM_BASEURL}',
|
||||
mistralModel: '${CUSTOM_MODEL}',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Set environment variable for model
|
||||
process.env.CUSTOM_MODEL = 'mistral-large';
|
||||
|
||||
const file = {
|
||||
path: '/tmp/upload/file.pdf',
|
||||
originalname: 'document.pdf',
|
||||
};
|
||||
|
||||
const result = await uploadMistralOCR({
|
||||
req,
|
||||
file,
|
||||
file_id: 'file123',
|
||||
entity_id: 'entity123',
|
||||
});
|
||||
|
||||
expect(fs.createReadStream).toHaveBeenCalledWith('/tmp/upload/file.pdf');
|
||||
|
||||
// Verify that custom environment variables were extracted and used
|
||||
expect(loadAuthValues).toHaveBeenCalledWith({
|
||||
userId: 'user123',
|
||||
authFields: ['CUSTOM_BASEURL', 'CUSTOM_API_KEY'],
|
||||
optional: expect.any(Set),
|
||||
});
|
||||
|
||||
// Check that mistral-large was used in the OCR API call
|
||||
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
model: 'mistral-large',
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
|
||||
expect(result.text).toEqual('Content from custom API\n\n');
|
||||
});
|
||||
|
||||
it('should fall back to default values when variables are not properly formatted', async () => {
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
loadAuthValues.mockResolvedValue({
|
||||
OCR_API_KEY: 'default-api-key',
|
||||
OCR_BASEURL: undefined, // Testing optional parameter
|
||||
});
|
||||
|
||||
mockAxios.post.mockResolvedValueOnce({
|
||||
data: { id: 'file-123', purpose: 'ocr' },
|
||||
});
|
||||
mockAxios.get.mockResolvedValueOnce({
|
||||
data: { url: 'https://signed-url.com' },
|
||||
});
|
||||
mockAxios.post.mockResolvedValueOnce({
|
||||
data: {
|
||||
pages: [{ markdown: 'Default API result' }],
|
||||
},
|
||||
});
|
||||
|
||||
const req = {
|
||||
user: { id: 'user123' },
|
||||
app: {
|
||||
locals: {
|
||||
ocr: {
|
||||
// Use environment variable syntax to ensure loadAuthValues is called
|
||||
apiKey: '${INVALID_FORMAT}', // Using valid env var format but with an invalid name
|
||||
baseURL: '${OCR_BASEURL}', // Using valid env var format
|
||||
mistralModel: 'mistral-ocr-latest', // Plain string value
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const file = {
|
||||
path: '/tmp/upload/file.pdf',
|
||||
originalname: 'document.pdf',
|
||||
};
|
||||
|
||||
await uploadMistralOCR({
|
||||
req,
|
||||
file,
|
||||
file_id: 'file123',
|
||||
entity_id: 'entity123',
|
||||
});
|
||||
|
||||
expect(fs.createReadStream).toHaveBeenCalledWith('/tmp/upload/file.pdf');
|
||||
|
||||
// Should use the default values
|
||||
expect(loadAuthValues).toHaveBeenCalledWith({
|
||||
userId: 'user123',
|
||||
authFields: ['OCR_BASEURL', 'INVALID_FORMAT'],
|
||||
optional: expect.any(Set),
|
||||
});
|
||||
|
||||
// Should use the default model when not using environment variable format
|
||||
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
model: 'mistral-ocr-latest',
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle API errors during OCR process', async () => {
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
loadAuthValues.mockResolvedValue({
|
||||
OCR_API_KEY: 'test-api-key',
|
||||
});
|
||||
|
||||
// Mock file upload to fail
|
||||
mockAxios.post.mockRejectedValueOnce(new Error('Upload failed'));
|
||||
|
||||
const req = {
|
||||
user: { id: 'user123' },
|
||||
app: {
|
||||
locals: {
|
||||
ocr: {
|
||||
apiKey: 'OCR_API_KEY',
|
||||
baseURL: 'OCR_BASEURL',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const file = {
|
||||
path: '/tmp/upload/file.pdf',
|
||||
originalname: 'document.pdf',
|
||||
};
|
||||
|
||||
await expect(
|
||||
uploadMistralOCR({
|
||||
req,
|
||||
file,
|
||||
file_id: 'file123',
|
||||
entity_id: 'entity123',
|
||||
}),
|
||||
).rejects.toThrow('Error uploading document to Mistral OCR API');
|
||||
expect(fs.createReadStream).toHaveBeenCalledWith('/tmp/upload/file.pdf');
|
||||
|
||||
const { logAxiosError } = require('~/utils');
|
||||
expect(logAxiosError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle single page documents without page numbering', async () => {
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
loadAuthValues.mockResolvedValue({
|
||||
OCR_API_KEY: 'test-api-key',
|
||||
OCR_BASEURL: 'https://api.mistral.ai/v1', // Make sure this is included
|
||||
});
|
||||
|
||||
// Clear all previous mocks
|
||||
mockAxios.post.mockClear();
|
||||
mockAxios.get.mockClear();
|
||||
|
||||
// 1. First mock: File upload response
|
||||
mockAxios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { id: 'file-123', purpose: 'ocr' } }),
|
||||
);
|
||||
|
||||
// 2. Second mock: Signed URL response
|
||||
mockAxios.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { url: 'https://signed-url.com' } }),
|
||||
);
|
||||
|
||||
// 3. Third mock: OCR response
|
||||
mockAxios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
pages: [{ markdown: 'Single page content' }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const req = {
|
||||
user: { id: 'user123' },
|
||||
app: {
|
||||
locals: {
|
||||
ocr: {
|
||||
apiKey: 'OCR_API_KEY',
|
||||
baseURL: 'OCR_BASEURL',
|
||||
mistralModel: 'mistral-ocr-latest',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const file = {
|
||||
path: '/tmp/upload/file.pdf',
|
||||
originalname: 'single-page.pdf',
|
||||
};
|
||||
|
||||
const result = await uploadMistralOCR({
|
||||
req,
|
||||
file,
|
||||
file_id: 'file123',
|
||||
entity_id: 'entity123',
|
||||
});
|
||||
|
||||
expect(fs.createReadStream).toHaveBeenCalledWith('/tmp/upload/file.pdf');
|
||||
|
||||
// Verify that single page documents don't include page numbering
|
||||
expect(result.text).not.toContain('# PAGE');
|
||||
expect(result.text).toEqual('Single page content\n\n');
|
||||
});
|
||||
|
||||
it('should use literal values in configuration when provided directly', async () => {
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
// We'll still mock this but it should not be used for literal values
|
||||
loadAuthValues.mockResolvedValue({});
|
||||
|
||||
// Clear all previous mocks
|
||||
mockAxios.post.mockClear();
|
||||
mockAxios.get.mockClear();
|
||||
|
||||
// 1. First mock: File upload response
|
||||
mockAxios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { id: 'file-123', purpose: 'ocr' } }),
|
||||
);
|
||||
|
||||
// 2. Second mock: Signed URL response
|
||||
mockAxios.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { url: 'https://signed-url.com' } }),
|
||||
);
|
||||
|
||||
// 3. Third mock: OCR response
|
||||
mockAxios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
pages: [{ markdown: 'Processed with literal config values' }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const req = {
|
||||
user: { id: 'user123' },
|
||||
app: {
|
||||
locals: {
|
||||
ocr: {
|
||||
// Direct values that should be used as-is, without variable substitution
|
||||
apiKey: 'actual-api-key-value',
|
||||
baseURL: 'https://direct-api-url.mistral.ai/v1',
|
||||
mistralModel: 'mistral-direct-model',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const file = {
|
||||
path: '/tmp/upload/file.pdf',
|
||||
originalname: 'direct-values.pdf',
|
||||
};
|
||||
|
||||
const result = await uploadMistralOCR({
|
||||
req,
|
||||
file,
|
||||
file_id: 'file123',
|
||||
entity_id: 'entity123',
|
||||
});
|
||||
|
||||
expect(fs.createReadStream).toHaveBeenCalledWith('/tmp/upload/file.pdf');
|
||||
|
||||
// Verify the correct URL was used with the direct baseURL value
|
||||
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||
'https://direct-api-url.mistral.ai/v1/files',
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer actual-api-key-value',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// Check the OCR call was made with the direct model value
|
||||
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||
'https://direct-api-url.mistral.ai/v1/ocr',
|
||||
expect.objectContaining({
|
||||
model: 'mistral-direct-model',
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
// Verify the result
|
||||
expect(result.text).toEqual('Processed with literal config values\n\n');
|
||||
|
||||
// Verify loadAuthValues was never called since we used direct values
|
||||
expect(loadAuthValues).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty configuration values and use defaults', async () => {
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
// Set up the mock values to be returned by loadAuthValues
|
||||
loadAuthValues.mockResolvedValue({
|
||||
OCR_API_KEY: 'default-from-env-key',
|
||||
OCR_BASEURL: 'https://default-from-env.mistral.ai/v1',
|
||||
});
|
||||
|
||||
// Clear all previous mocks
|
||||
mockAxios.post.mockClear();
|
||||
mockAxios.get.mockClear();
|
||||
|
||||
// 1. First mock: File upload response
|
||||
mockAxios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { id: 'file-123', purpose: 'ocr' } }),
|
||||
);
|
||||
|
||||
// 2. Second mock: Signed URL response
|
||||
mockAxios.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { url: 'https://signed-url.com' } }),
|
||||
);
|
||||
|
||||
// 3. Third mock: OCR response
|
||||
mockAxios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
pages: [{ markdown: 'Content from default configuration' }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const req = {
|
||||
user: { id: 'user123' },
|
||||
app: {
|
||||
locals: {
|
||||
ocr: {
|
||||
// Empty string values - should fall back to defaults
|
||||
apiKey: '',
|
||||
baseURL: '',
|
||||
mistralModel: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const file = {
|
||||
path: '/tmp/upload/file.pdf',
|
||||
originalname: 'empty-config.pdf',
|
||||
};
|
||||
|
||||
const result = await uploadMistralOCR({
|
||||
req,
|
||||
file,
|
||||
file_id: 'file123',
|
||||
entity_id: 'entity123',
|
||||
});
|
||||
|
||||
expect(fs.createReadStream).toHaveBeenCalledWith('/tmp/upload/file.pdf');
|
||||
|
||||
// Verify loadAuthValues was called with the default variable names
|
||||
expect(loadAuthValues).toHaveBeenCalledWith({
|
||||
userId: 'user123',
|
||||
authFields: ['OCR_BASEURL', 'OCR_API_KEY'],
|
||||
optional: expect.any(Set),
|
||||
});
|
||||
|
||||
// Verify the API calls used the default values from loadAuthValues
|
||||
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||
'https://default-from-env.mistral.ai/v1/files',
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer default-from-env-key',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify the OCR model defaulted to mistral-ocr-latest
|
||||
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||
'https://default-from-env.mistral.ai/v1/ocr',
|
||||
expect.objectContaining({
|
||||
model: 'mistral-ocr-latest',
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
// Check result
|
||||
expect(result.text).toEqual('Content from default configuration\n\n');
|
||||
});
|
||||
});
|
||||
});
|
||||
5
api/server/services/Files/MistralOCR/index.js
Normal file
5
api/server/services/Files/MistralOCR/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const crud = require('./crud');
|
||||
|
||||
module.exports = {
|
||||
...crud,
|
||||
};
|
||||
@@ -49,6 +49,7 @@ async function encodeAndFormat(req, files, endpoint, mode) {
|
||||
const promises = [];
|
||||
const encodingMethods = {};
|
||||
const result = {
|
||||
text: '',
|
||||
files: [],
|
||||
image_urls: [],
|
||||
};
|
||||
@@ -59,6 +60,9 @@ async function encodeAndFormat(req, files, endpoint, mode) {
|
||||
|
||||
for (let file of files) {
|
||||
const source = file.source ?? FileSources.local;
|
||||
if (source === FileSources.text && file.text) {
|
||||
result.text += `${!result.text ? 'Attached document(s):\n```md' : '\n\n---\n\n'}# "${file.filename}"\n${file.text}\n`;
|
||||
}
|
||||
|
||||
if (!file.height) {
|
||||
promises.push([file, null]);
|
||||
@@ -85,6 +89,10 @@ async function encodeAndFormat(req, files, endpoint, mode) {
|
||||
promises.push(preparePayload(req, file));
|
||||
}
|
||||
|
||||
if (result.text) {
|
||||
result.text += '\n```';
|
||||
}
|
||||
|
||||
const detail = req.body.imageDetail ?? ImageDetail.auto;
|
||||
|
||||
/** @type {Array<[MongoFile, string]>} */
|
||||
|
||||
@@ -28,8 +28,8 @@ const { addResourceFileId, deleteResourceFileId } = require('~/server/controller
|
||||
const { addAgentResourceFile, removeAgentResourceFiles } = require('~/models/Agent');
|
||||
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
||||
const { createFile, updateFileUsage, deleteFiles } = require('~/models/File');
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
const { getEndpointsConfig } = require('~/server/services/Config');
|
||||
const { loadAuthValues } = require('~/app/clients/tools/util');
|
||||
const { LB_QueueAsyncCall } = require('~/server/utils/queue');
|
||||
const { getStrategyFunctions } = require('./strategies');
|
||||
const { determineFileType } = require('~/server/utils');
|
||||
@@ -162,7 +162,6 @@ 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) {
|
||||
agentFiles.push({
|
||||
tool_resource: req.body.tool_resource,
|
||||
@@ -170,6 +169,11 @@ const processDeleteRequest = async ({ req, files }) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (source === FileSources.text) {
|
||||
resolvedFileIds.push(file.file_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checkOpenAIStorage(source) && !client[source]) {
|
||||
await initializeClients();
|
||||
}
|
||||
@@ -521,6 +525,52 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
|
||||
if (!isFileSearchEnabled) {
|
||||
throw new Error('File search is not enabled for Agents');
|
||||
}
|
||||
} else if (tool_resource === EToolResources.ocr) {
|
||||
const isOCREnabled = await checkCapability(req, AgentCapabilities.ocr);
|
||||
if (!isOCREnabled) {
|
||||
throw new Error('OCR capability is not enabled for Agents');
|
||||
}
|
||||
|
||||
const { handleFileUpload } = getStrategyFunctions(
|
||||
req.app.locals?.ocr?.strategy ?? FileSources.mistral_ocr,
|
||||
);
|
||||
const { file_id, temp_file_id } = metadata;
|
||||
|
||||
const {
|
||||
text,
|
||||
bytes,
|
||||
// TODO: OCR images support?
|
||||
images,
|
||||
filename,
|
||||
filepath: ocrFileURL,
|
||||
} = await handleFileUpload({ req, file, file_id, entity_id: agent_id });
|
||||
|
||||
const fileInfo = removeNullishValues({
|
||||
text,
|
||||
bytes,
|
||||
file_id,
|
||||
temp_file_id,
|
||||
user: req.user.id,
|
||||
type: file.mimetype,
|
||||
filepath: ocrFileURL,
|
||||
source: FileSources.text,
|
||||
filename: filename ?? file.originalname,
|
||||
model: messageAttachment ? undefined : req.body.model,
|
||||
context: messageAttachment ? FileContext.message_attachment : FileContext.agents,
|
||||
});
|
||||
|
||||
if (!messageAttachment && tool_resource) {
|
||||
await addAgentResourceFile({
|
||||
req,
|
||||
file_id,
|
||||
agent_id,
|
||||
tool_resource,
|
||||
});
|
||||
}
|
||||
const result = await createFile(fileInfo, true);
|
||||
return res
|
||||
.status(200)
|
||||
.json({ message: 'Agent file uploaded and processed successfully', ...result });
|
||||
}
|
||||
|
||||
const source =
|
||||
|
||||
@@ -24,6 +24,7 @@ const {
|
||||
const { uploadOpenAIFile, deleteOpenAIFile, getOpenAIFileStream } = require('./OpenAI');
|
||||
const { getCodeOutputDownloadStream, uploadCodeEnvFile } = require('./Code');
|
||||
const { uploadVectors, deleteVectors } = require('./VectorDB');
|
||||
const { uploadMistralOCR } = require('./MistralOCR');
|
||||
|
||||
/**
|
||||
* Firebase Storage Strategy Functions
|
||||
@@ -127,6 +128,26 @@ const codeOutputStrategy = () => ({
|
||||
getDownloadStream: getCodeOutputDownloadStream,
|
||||
});
|
||||
|
||||
const mistralOCRStrategy = () => ({
|
||||
/** @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 getLocalFileStream | null} */
|
||||
getDownloadStream: null,
|
||||
handleFileUpload: uploadMistralOCR,
|
||||
});
|
||||
|
||||
// Strategy Selector
|
||||
const getStrategyFunctions = (fileSource) => {
|
||||
if (fileSource === FileSources.firebase) {
|
||||
@@ -141,6 +162,8 @@ const getStrategyFunctions = (fileSource) => {
|
||||
return vectorStrategy();
|
||||
} else if (fileSource === FileSources.execute_code) {
|
||||
return codeOutputStrategy();
|
||||
} else if (fileSource === FileSources.mistral_ocr) {
|
||||
return mistralOCRStrategy();
|
||||
} else {
|
||||
throw new Error('Invalid file source');
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ const fetchOpenAIModels = async (opts, _models = []) => {
|
||||
}
|
||||
|
||||
if (baseURL === openaiBaseURL) {
|
||||
const regex = /(text-davinci-003|gpt-|o\d+-)/;
|
||||
const regex = /(text-davinci-003|gpt-|o\d+)/;
|
||||
const excludeRegex = /audio|realtime/;
|
||||
models = models.filter((model) => regex.test(model) && !excludeRegex.test(model));
|
||||
const instructModels = models.filter((model) => model.includes('instruct'));
|
||||
|
||||
56
api/server/services/Tools/credentials.js
Normal file
56
api/server/services/Tools/credentials.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {string} params.userId
|
||||
* @param {string[]} params.authFields
|
||||
* @param {Set<string>} [params.optional]
|
||||
* @param {boolean} [params.throwError]
|
||||
* @returns
|
||||
*/
|
||||
const loadAuthValues = async ({ userId, authFields, optional, throwError = true }) => {
|
||||
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, throwError);
|
||||
} catch (err) {
|
||||
if (optional && optional.has(field)) {
|
||||
return { authField: field, authValue: undefined };
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
loadAuthValues,
|
||||
};
|
||||
@@ -203,6 +203,7 @@ function generateConfig(key, baseURL, endpoint) {
|
||||
AgentCapabilities.artifacts,
|
||||
AgentCapabilities.actions,
|
||||
AgentCapabilities.tools,
|
||||
AgentCapabilities.ocr,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ jest.mock('winston-daily-rotate-file', () => {
|
||||
});
|
||||
|
||||
jest.mock('~/config', () => {
|
||||
const actualModule = jest.requireActual('~/config');
|
||||
return {
|
||||
sendEvent: actualModule.sendEvent,
|
||||
createAxiosInstance: actualModule.createAxiosInstance,
|
||||
logger: {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
|
||||
@@ -766,36 +766,6 @@
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports MongoFile
|
||||
* @typedef {import('~/models/schema/fileSchema.js').MongoFile} MongoFile
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports ToolCallData
|
||||
* @typedef {import('~/models/schema/toolCallSchema.js').ToolCallData} ToolCallData
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports MongoUser
|
||||
* @typedef {import('~/models/schema/userSchema.js').MongoUser} MongoUser
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports MongoProject
|
||||
* @typedef {import('~/models/schema/projectSchema.js').MongoProject} MongoProject
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports MongoPromptGroup
|
||||
* @typedef {import('~/models/schema/promptSchema.js').MongoPromptGroup} MongoPromptGroup
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports uploadImageBuffer
|
||||
* @typedef {import('~/server/services/Files/process').uploadImageBuffer} uploadImageBuffer
|
||||
@@ -1817,3 +1787,51 @@
|
||||
* @typedef {Promise<{ message: TMessage, conversation: TConversation }> | undefined} ClientDatabaseSavePromise
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports OCRImage
|
||||
* @typedef {Object} OCRImage
|
||||
* @property {string} id - The identifier of the image.
|
||||
* @property {number} top_left_x - X-coordinate of the top left corner of the image.
|
||||
* @property {number} top_left_y - Y-coordinate of the top left corner of the image.
|
||||
* @property {number} bottom_right_x - X-coordinate of the bottom right corner of the image.
|
||||
* @property {number} bottom_right_y - Y-coordinate of the bottom right corner of the image.
|
||||
* @property {string} image_base64 - Base64-encoded image data.
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports PageDimensions
|
||||
* @typedef {Object} PageDimensions
|
||||
* @property {number} dpi - The dots per inch resolution of the page.
|
||||
* @property {number} height - The height of the page in pixels.
|
||||
* @property {number} width - The width of the page in pixels.
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports OCRPage
|
||||
* @typedef {Object} OCRPage
|
||||
* @property {number} index - The index of the page in the document.
|
||||
* @property {string} markdown - The extracted text content of the page in markdown format.
|
||||
* @property {OCRImage[]} images - Array of images found on the page.
|
||||
* @property {PageDimensions} dimensions - The dimensions of the page.
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports OCRUsageInfo
|
||||
* @typedef {Object} OCRUsageInfo
|
||||
* @property {number} pages_processed - Number of pages processed in the document.
|
||||
* @property {number} doc_size_bytes - Size of the document in bytes.
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports OCRResult
|
||||
* @typedef {Object} OCRResult
|
||||
* @property {OCRPage[]} pages - Array of pages extracted from the document.
|
||||
* @property {string} model - The model used for OCR processing.
|
||||
* @property {OCRUsageInfo} usage_info - Usage information for the OCR operation.
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { OptionWithIcon, ExtendedFile } from './types';
|
||||
export type TAgentOption = OptionWithIcon &
|
||||
Agent & {
|
||||
knowledge_files?: Array<[string, ExtendedFile]>;
|
||||
context_files?: Array<[string, ExtendedFile]>;
|
||||
code_files?: Array<[string, ExtendedFile]>;
|
||||
};
|
||||
|
||||
|
||||
@@ -483,6 +483,7 @@ export interface ExtendedFile {
|
||||
attached?: boolean;
|
||||
embedded?: boolean;
|
||||
tool_resource?: string;
|
||||
metadata?: t.TFile['metadata'];
|
||||
}
|
||||
|
||||
export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void };
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useMemo, memo, useCallback } from 'react';
|
||||
import React, { useState, useMemo, memo, useCallback } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Atom, ChevronDown } from 'lucide-react';
|
||||
import type { MouseEvent, FC } from 'react';
|
||||
import Markdown from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
@@ -21,12 +22,18 @@ const CONTENT_STYLES = {
|
||||
} as const;
|
||||
|
||||
export const ThinkingContent: FC<{ children: React.ReactNode; isPart?: boolean }> = memo(
|
||||
({ isPart, children }) => (
|
||||
<div className={CONTENT_STYLES.wrapper}>
|
||||
<div className={isPart === true ? CONTENT_STYLES.partBorder : CONTENT_STYLES.border} />
|
||||
<p className={CONTENT_STYLES.text}>{children}</p>
|
||||
</div>
|
||||
),
|
||||
({ isPart, children }) => {
|
||||
return (
|
||||
<div className={CONTENT_STYLES.wrapper}>
|
||||
<div className={isPart === true ? CONTENT_STYLES.partBorder : CONTENT_STYLES.border} />
|
||||
{typeof children === 'string' ? (
|
||||
<Markdown content={children} isLatestMessage={false} />
|
||||
) : (
|
||||
<p className={CONTENT_STYLES.text}>{children}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ThinkingButton = memo(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import React, { useRef, useState, useMemo } from 'react';
|
||||
import { FileSearch, ImageUpIcon, TerminalSquareIcon } from 'lucide-react';
|
||||
import { EToolResources, EModelEndpoint } from 'librechat-data-provider';
|
||||
import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
|
||||
import { FileUpload, TooltipAnchor, DropdownPopup } from '~/components/ui';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import { AttachmentIcon } from '~/components/svg';
|
||||
@@ -49,6 +49,17 @@ const AttachFile = ({ isRTL, disabled, handleFileChange }: AttachFileProps) => {
|
||||
},
|
||||
];
|
||||
|
||||
if (capabilities.includes(EToolResources.ocr)) {
|
||||
items.push({
|
||||
label: localize('com_ui_upload_ocr_text'),
|
||||
onClick: () => {
|
||||
setToolResource(EToolResources.ocr);
|
||||
handleUploadClick();
|
||||
},
|
||||
icon: <FileType2Icon className="icon-md" />,
|
||||
});
|
||||
}
|
||||
|
||||
if (capabilities.includes(EToolResources.file_search)) {
|
||||
items.push({
|
||||
label: localize('com_ui_upload_file_search'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { EModelEndpoint, EToolResources } from 'librechat-data-provider';
|
||||
import { FileSearch, ImageUpIcon, TerminalSquareIcon } from 'lucide-react';
|
||||
import { FileSearch, ImageUpIcon, FileType2Icon, TerminalSquareIcon } from 'lucide-react';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
@@ -50,6 +50,12 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD
|
||||
value: EToolResources.execute_code,
|
||||
icon: <TerminalSquareIcon className="icon-md" />,
|
||||
});
|
||||
} else if (capability === EToolResources.ocr) {
|
||||
_options.push({
|
||||
label: localize('com_ui_upload_ocr_text'),
|
||||
value: EToolResources.ocr,
|
||||
icon: <FileType2Icon className="icon-md" />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const FilePreview = ({
|
||||
};
|
||||
className?: string;
|
||||
}) => {
|
||||
const radius = 55; // Radius of the SVG circle
|
||||
const radius = 55;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const progress = useProgress(
|
||||
file?.['progress'] ?? 1,
|
||||
@@ -27,16 +27,15 @@ const FilePreview = ({
|
||||
(file as ExtendedFile | undefined)?.size ?? 1,
|
||||
);
|
||||
|
||||
// Calculate the offset based on the loading progress
|
||||
const offset = circumference - progress * circumference;
|
||||
const circleCSSProperties = {
|
||||
transition: 'stroke-dashoffset 0.5s linear',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('size-10 shrink-0 overflow-hidden rounded-xl', className)}>
|
||||
<div className={cn('relative size-10 shrink-0 overflow-hidden rounded-xl', className)}>
|
||||
<FileIcon file={file} fileType={fileType} />
|
||||
<SourceIcon source={file?.source} />
|
||||
<SourceIcon source={file?.source} isCodeFile={!!file?.['metadata']?.fileIdentifier} />
|
||||
{progress < 1 && (
|
||||
<ProgressCircle
|
||||
circumference={circumference}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Terminal, Type, Database } from 'lucide-react';
|
||||
import { EModelEndpoint, FileSources } from 'librechat-data-provider';
|
||||
import { MinimalIcon } from '~/components/Endpoints';
|
||||
import { cn } from '~/utils';
|
||||
@@ -6,9 +7,13 @@ const sourceToEndpoint = {
|
||||
[FileSources.openai]: EModelEndpoint.openAI,
|
||||
[FileSources.azure]: EModelEndpoint.azureOpenAI,
|
||||
};
|
||||
|
||||
const sourceToClassname = {
|
||||
[FileSources.openai]: 'bg-white/75 dark:bg-black/65',
|
||||
[FileSources.azure]: 'azure-bg-color opacity-85',
|
||||
[FileSources.execute_code]: 'bg-black text-white opacity-85',
|
||||
[FileSources.text]: 'bg-blue-100 dark:bg-blue-900 opacity-85 text-white',
|
||||
[FileSources.vectordb]: 'bg-yellow-100 dark:bg-yellow-900 opacity-85 text-white',
|
||||
};
|
||||
|
||||
const defaultClassName =
|
||||
@@ -16,13 +21,41 @@ const defaultClassName =
|
||||
|
||||
export default function SourceIcon({
|
||||
source,
|
||||
isCodeFile,
|
||||
className = defaultClassName,
|
||||
}: {
|
||||
source?: FileSources;
|
||||
isCodeFile?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
if (source === FileSources.local || source === FileSources.firebase) {
|
||||
return null;
|
||||
if (isCodeFile === true) {
|
||||
return (
|
||||
<div className={cn(className, sourceToClassname[FileSources.execute_code] ?? '')}>
|
||||
<span className="flex items-center justify-center">
|
||||
<Terminal className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (source === FileSources.text) {
|
||||
return (
|
||||
<div className={cn(className, sourceToClassname[source] ?? '')}>
|
||||
<span className="flex items-center justify-center">
|
||||
<Type className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (source === FileSources.vectordb) {
|
||||
return (
|
||||
<div className={cn(className, sourceToClassname[source] ?? '')}>
|
||||
<span className="flex items-center justify-center">
|
||||
<Database className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const endpoint = sourceToEndpoint[source ?? ''];
|
||||
@@ -31,7 +64,7 @@ export default function SourceIcon({
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<button type="button" className={cn(className, sourceToClassname[source ?? ''] ?? '')}>
|
||||
<div className={cn(className, sourceToClassname[source ?? ''] ?? '')}>
|
||||
<span className="flex items-center justify-center">
|
||||
<MinimalIcon
|
||||
endpoint={endpoint}
|
||||
@@ -40,6 +73,6 @@ export default function SourceIcon({
|
||||
iconClassName="h-3 w-3"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ const MenuItem: FC<MenuItemProps> = ({
|
||||
{showIconInMenu && <SpecIcon currentSpec={spec} endpointsConfig={endpointsConfig} />}
|
||||
<div>
|
||||
{title}
|
||||
<div className="text-token-text-tertiary">{description}</div>
|
||||
<div className="text-text-secondary">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,6 +166,46 @@ export const p: React.ElementType = memo(({ children }: TParagraphProps) => {
|
||||
return <p className="mb-2 whitespace-pre-wrap">{children}</p>;
|
||||
});
|
||||
|
||||
export const h1: React.ElementType = memo(({ children, ...props }: TParagraphProps) => {
|
||||
return (
|
||||
<h1 className="mb-2 mt-4 text-3xl font-bold" {...props}>
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
});
|
||||
|
||||
export const h2: React.ElementType = memo(({ children, ...props }: TParagraphProps) => {
|
||||
return (
|
||||
<h2 className="mb-2 mt-4 text-2xl font-bold" {...props}>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
});
|
||||
|
||||
export const h3: React.ElementType = memo(({ children, ...props }: TParagraphProps) => {
|
||||
return (
|
||||
<h3 className="mb-1 mt-3 text-xl font-bold" {...props}>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
});
|
||||
|
||||
export const ol: React.ElementType = memo(({ children, ...props }: TParagraphProps) => {
|
||||
return (
|
||||
<ol className="ml-6 list-decimal" {...props}>
|
||||
{children}
|
||||
</ol>
|
||||
);
|
||||
});
|
||||
|
||||
export const ul: React.ElementType = memo(({ children, ...props }: TParagraphProps) => {
|
||||
return (
|
||||
<ul className="ml-6 list-disc" {...props}>
|
||||
{children}
|
||||
</ul>
|
||||
);
|
||||
});
|
||||
|
||||
const cursor = ' ';
|
||||
|
||||
type TContentProps = {
|
||||
@@ -234,6 +274,11 @@ const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentPr
|
||||
code,
|
||||
a,
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
ol,
|
||||
ul,
|
||||
artifact: Artifact,
|
||||
} as {
|
||||
[nodeType: string]: React.ElementType;
|
||||
|
||||
@@ -32,7 +32,12 @@ const Part = memo(({ part, isSubmitting, attachments, showCursor, isCreatedByUse
|
||||
}
|
||||
|
||||
if (part.type === ContentTypes.ERROR) {
|
||||
return <ErrorMessage text={part[ContentTypes.TEXT].value} className="my-2" />;
|
||||
return (
|
||||
<ErrorMessage
|
||||
text={part[ContentTypes.ERROR] ?? part[ContentTypes.TEXT]?.value}
|
||||
className="my-2"
|
||||
/>
|
||||
);
|
||||
} else if (part.type === ContentTypes.TEXT) {
|
||||
const text = typeof part.text === 'string' ? part.text : part.text.value;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ function Account() {
|
||||
<div className="pb-3">
|
||||
<EnableTwoFactorItem />
|
||||
</div>
|
||||
{Array.isArray(user.user?.backupCodes) && user.user?.backupCodes.length > 0 && (
|
||||
{user?.user?.twoFactorEnabled && (
|
||||
<div className="pb-3">
|
||||
<BackupCodesItem />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { LockIcon, UnlockIcon } from 'lucide-react';
|
||||
// import { motion } from 'framer-motion';
|
||||
// import { LockIcon, UnlockIcon } from 'lucide-react';
|
||||
import { Label, Button } from '~/components';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const TwoFactorAuthentication: React.FC = () => {
|
||||
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
||||
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [verificationToken, setVerificationToken] = useState<string>('');
|
||||
const [phase, setPhase] = useState<Phase>(Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? 'disable' : 'setup');
|
||||
const [phase, setPhase] = useState<Phase>(user?.twoFactorEnabled ? 'disable' : 'setup');
|
||||
|
||||
const { mutate: confirm2FAMutate } = useConfirmTwoFactorMutation();
|
||||
const { mutate: enable2FAMutate, isLoading: isGenerating } = useEnableTwoFactorMutation();
|
||||
@@ -56,7 +56,7 @@ const TwoFactorAuthentication: React.FC = () => {
|
||||
const currentStep = steps.indexOf(phasesLabel[phase]);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
if (Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 && otpauthUrl) {
|
||||
if (user?.twoFactorEnabled && otpauthUrl) {
|
||||
disable2FAMutate(undefined, {
|
||||
onError: () =>
|
||||
showToast({ message: localize('com_ui_2fa_disable_error'), status: 'error' }),
|
||||
@@ -68,7 +68,7 @@ const TwoFactorAuthentication: React.FC = () => {
|
||||
setBackupCodes([]);
|
||||
setVerificationToken('');
|
||||
setDisableToken('');
|
||||
setPhase(Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? 'disable' : 'setup');
|
||||
setPhase(user?.twoFactorEnabled ? 'disable' : 'setup');
|
||||
setDownloaded(false);
|
||||
}, [user, otpauthUrl, disable2FAMutate, localize, showToast]);
|
||||
|
||||
@@ -136,6 +136,7 @@ const TwoFactorAuthentication: React.FC = () => {
|
||||
used: false,
|
||||
usedAt: null,
|
||||
})),
|
||||
twoFactorEnabled: true,
|
||||
}) as TUser,
|
||||
);
|
||||
}, [setUser, localize, showToast, backupCodes]);
|
||||
@@ -171,6 +172,7 @@ const TwoFactorAuthentication: React.FC = () => {
|
||||
...prev,
|
||||
totpSecret: '',
|
||||
backupCodes: [],
|
||||
twoFactorEnabled: false,
|
||||
}) as TUser,
|
||||
);
|
||||
setPhase('setup');
|
||||
@@ -183,7 +185,7 @@ const TwoFactorAuthentication: React.FC = () => {
|
||||
onError: () => showToast({ message: localize('com_ui_2fa_invalid'), status: 'error' }),
|
||||
});
|
||||
},
|
||||
[disableToken, verify2FAMutate, disable2FAMutate, showToast, localize, setUser],
|
||||
[verify2FAMutate, disable2FAMutate, showToast, localize, setUser],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -197,7 +199,7 @@ const TwoFactorAuthentication: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<DisableTwoFactorToggle
|
||||
enabled={Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0}
|
||||
enabled={!!user?.twoFactorEnabled}
|
||||
onChange={() => setDialogOpen(true)}
|
||||
disabled={isVerifying || isDisabling || isGenerating}
|
||||
/>
|
||||
@@ -215,9 +217,11 @@ const TwoFactorAuthentication: React.FC = () => {
|
||||
<OGDialogHeader>
|
||||
<OGDialogTitle className="mb-2 flex items-center gap-3 text-2xl font-bold">
|
||||
<SmartphoneIcon className="h-6 w-6 text-primary" />
|
||||
{Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? localize('com_ui_2fa_disable') : localize('com_ui_2fa_setup')}
|
||||
{user?.twoFactorEnabled
|
||||
? localize('com_ui_2fa_disable')
|
||||
: localize('com_ui_2fa_setup')}
|
||||
</OGDialogTitle>
|
||||
{Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 && phase !== 'disable' && (
|
||||
{user?.twoFactorEnabled && phase !== 'disable' && (
|
||||
<div className="mt-4 space-y-3">
|
||||
<Progress
|
||||
value={(steps.indexOf(phasesLabel[phase]) / (steps.length - 1)) * 100}
|
||||
|
||||
@@ -74,7 +74,7 @@ export const DisablePhase: React.FC<DisablePhaseProps> = ({ onDisable, isDisabli
|
||||
disabled={isDisabling || token.length !== (useBackup ? 8 : 6)}
|
||||
className="w-full rounded-xl px-6 py-3 transition-all disabled:opacity-50"
|
||||
>
|
||||
{isDisabling === true && <Spinner className="mr-2" />}
|
||||
{isDisabling && <Spinner className="mr-2" />}
|
||||
{isDisabling ? localize('com_ui_disabling') : localize('com_ui_2fa_disable')}
|
||||
</Button>
|
||||
<button
|
||||
|
||||
@@ -18,7 +18,7 @@ interface SetupPhaseProps {
|
||||
onGenerate: () => void;
|
||||
}
|
||||
|
||||
export const SetupPhase: React.FC<SetupPhaseProps> = ({ isGenerating, onGenerate, onNext }) => {
|
||||
export const SetupPhase: React.FC<SetupPhaseProps> = ({ isGenerating, onGenerate }) => {
|
||||
const localize = useLocalize();
|
||||
|
||||
return (
|
||||
|
||||
@@ -68,6 +68,7 @@ export const LangSelector = ({
|
||||
{ value: 'sv-SE', label: localize('com_nav_lang_swedish') },
|
||||
{ value: 'ko-KR', label: localize('com_nav_lang_korean') },
|
||||
{ value: 'vi-VN', label: localize('com_nav_lang_vietnamese') },
|
||||
{ value: 'th-TH', label: localize('com_nav_lang_thai') },
|
||||
{ value: 'tr-TR', label: localize('com_nav_lang_turkish') },
|
||||
{ value: 'nl-NL', label: localize('com_nav_lang_dutch') },
|
||||
{ value: 'id-ID', label: localize('com_nav_lang_indonesia') },
|
||||
|
||||
@@ -23,6 +23,7 @@ import { processAgentOption } from '~/utils';
|
||||
import AdminSettings from './AdminSettings';
|
||||
import DeleteButton from './DeleteButton';
|
||||
import AgentAvatar from './AgentAvatar';
|
||||
import FileContext from './FileContext';
|
||||
import { Spinner } from '~/components';
|
||||
import FileSearch from './FileSearch';
|
||||
import ShareAgent from './ShareAgent';
|
||||
@@ -82,6 +83,10 @@ export default function AgentConfig({
|
||||
() => agentsConfig?.capabilities.includes(AgentCapabilities.artifacts) ?? false,
|
||||
[agentsConfig],
|
||||
);
|
||||
const ocrEnabled = useMemo(
|
||||
() => agentsConfig?.capabilities.includes(AgentCapabilities.ocr) ?? false,
|
||||
[agentsConfig],
|
||||
);
|
||||
const fileSearchEnabled = useMemo(
|
||||
() => agentsConfig?.capabilities.includes(AgentCapabilities.file_search) ?? false,
|
||||
[agentsConfig],
|
||||
@@ -91,6 +96,26 @@ export default function AgentConfig({
|
||||
[agentsConfig],
|
||||
);
|
||||
|
||||
const context_files = useMemo(() => {
|
||||
if (typeof agent === 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (agent?.id !== agent_id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (agent.context_files) {
|
||||
return agent.context_files;
|
||||
}
|
||||
|
||||
const _agent = processAgentOption({
|
||||
agent,
|
||||
fileMap,
|
||||
});
|
||||
return _agent.context_files ?? [];
|
||||
}, [agent, agent_id, fileMap]);
|
||||
|
||||
const knowledge_files = useMemo(() => {
|
||||
if (typeof agent === 'string') {
|
||||
return [];
|
||||
@@ -334,7 +359,7 @@ export default function AgentConfig({
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{(codeEnabled || fileSearchEnabled || artifactsEnabled) && (
|
||||
{(codeEnabled || fileSearchEnabled || artifactsEnabled || ocrEnabled) && (
|
||||
<div className="mb-4 flex w-full flex-col items-start gap-3">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_assistants_capabilities')}
|
||||
@@ -345,6 +370,8 @@ export default function AgentConfig({
|
||||
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
|
||||
{/* Artifacts */}
|
||||
{artifactsEnabled && <Artifacts />}
|
||||
{/* File Context (OCR) */}
|
||||
{ocrEnabled && <FileContext agent_id={agent_id} files={context_files} />}
|
||||
</div>
|
||||
)}
|
||||
{/* Agent Tools & Actions */}
|
||||
|
||||
128
client/src/components/SidePanel/Agents/FileContext.tsx
Normal file
128
client/src/components/SidePanel/Agents/FileContext.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import {
|
||||
EModelEndpoint,
|
||||
EToolResources,
|
||||
mergeFileConfig,
|
||||
fileConfig as defaultFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import { useFileHandling, useLocalize, useLazyEffect } from '~/hooks';
|
||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { HoverCard, HoverCardContent, HoverCardPortal, HoverCardTrigger } from '~/components/ui';
|
||||
import { AttachmentIcon, CircleHelpIcon } from '~/components/svg';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
export default function FileContext({
|
||||
agent_id,
|
||||
files: _files,
|
||||
}: {
|
||||
agent_id: string;
|
||||
files?: [string, ExtendedFile][];
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { setFilesLoading } = useChatContext();
|
||||
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.ocr },
|
||||
fileSetter: setFiles,
|
||||
});
|
||||
|
||||
useLazyEffect(
|
||||
() => {
|
||||
if (_files) {
|
||||
setFiles(new Map(_files));
|
||||
}
|
||||
},
|
||||
[_files],
|
||||
750,
|
||||
);
|
||||
|
||||
const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.agents];
|
||||
const isUploadDisabled = endpointFileConfig.disabled ?? false;
|
||||
|
||||
if (isUploadDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleButtonClick = () => {
|
||||
// necessary to reset the input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<HoverCard openDelay={50}>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<HoverCardTrigger asChild>
|
||||
<span className="flex items-center gap-2">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_agents_file_context')}
|
||||
</label>
|
||||
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
|
||||
</span>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent side={ESide.Top} className="w-80">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-text-secondary">
|
||||
{localize('com_agents_file_context_info')}
|
||||
</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
</div>
|
||||
</HoverCard>
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* File Context (OCR) Files */}
|
||||
<FileRow
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
setFilesLoading={setFilesLoading}
|
||||
agent_id={agent_id}
|
||||
tool_resource={EToolResources.ocr}
|
||||
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
|
||||
/>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!agent_id}
|
||||
className="btn btn-neutral border-token-border-light relative h-9 w-full 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}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
{localize('com_ui_upload_file_context')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/* Disabled Message */}
|
||||
{agent_id ? null : (
|
||||
<div className="text-xs text-text-secondary">
|
||||
{localize('com_agents_file_context_disabled')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
import { ArrowUpDown } from 'lucide-react';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import type { TFile } from 'librechat-data-provider';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import PanelFileCell from './PanelFileCell';
|
||||
import { Button } from '~/components/ui';
|
||||
import { formatDate } from '~/utils';
|
||||
|
||||
export const columns: ColumnDef<TFile>[] = [
|
||||
export const columns: ColumnDef<TFile | undefined>[] = [
|
||||
{
|
||||
accessorKey: 'filename',
|
||||
header: ({ column }) => {
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-surface-hover"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
>
|
||||
Name
|
||||
{localize('com_ui_name')}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
@@ -31,20 +33,21 @@ export const columns: ColumnDef<TFile>[] = [
|
||||
size: '10%',
|
||||
},
|
||||
header: ({ column }) => {
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-surface-hover"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
>
|
||||
Date
|
||||
{localize('com_ui_date')}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<span className="flex justify-end text-xs">
|
||||
{formatDate(row.original.updatedAt?.toString() ?? '')}
|
||||
{formatDate(row.original?.updatedAt?.toString() ?? '')}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ import { getFileType } from '~/utils';
|
||||
|
||||
export default function PanelFileCell({ row }: { row: Row<TFile | undefined> }) {
|
||||
const file = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{file?.type.startsWith('image') === true ? (
|
||||
|
||||
@@ -159,6 +159,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
||||
filename: fileData.filename,
|
||||
source: fileData.source,
|
||||
size: fileData.bytes,
|
||||
metadata: fileData.metadata,
|
||||
});
|
||||
},
|
||||
[addFile, fileMap, conversation, localize, showToast, fileConfig.endpoints],
|
||||
|
||||
@@ -63,8 +63,9 @@ export const useUploadFileMutation = (
|
||||
|
||||
const update = {};
|
||||
const prevResources = agent.tool_resources ?? {};
|
||||
const prevResource: t.ExecuteCodeResource | t.AgentFileSearchResource = agent
|
||||
.tool_resources?.[tool_resource] ?? {
|
||||
const prevResource: t.ExecuteCodeResource | t.AgentFileResource = agent.tool_resources?.[
|
||||
tool_resource
|
||||
] ?? {
|
||||
file_ids: [],
|
||||
};
|
||||
if (!prevResource.file_ids) {
|
||||
|
||||
@@ -337,6 +337,7 @@
|
||||
"com_nav_lang_estonian": "Eesti keel",
|
||||
"com_nav_lang_finnish": "Suomi",
|
||||
"com_nav_lang_french": "Français ",
|
||||
"com_nav_lang_georgian": "ქართული",
|
||||
"com_nav_lang_german": "Deutsch",
|
||||
"com_nav_lang_hebrew": "עברית",
|
||||
"com_nav_lang_indonesia": "Indonesia",
|
||||
@@ -348,6 +349,7 @@
|
||||
"com_nav_lang_russian": "Русский",
|
||||
"com_nav_lang_spanish": "Español",
|
||||
"com_nav_lang_swedish": "Svenska",
|
||||
"com_nav_lang_thai": "ไทย",
|
||||
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||
"com_nav_lang_turkish": "Türkçe",
|
||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||
|
||||
744
client/src/locales/cs/translation.json
Normal file
744
client/src/locales/cs/translation.json
Normal file
@@ -0,0 +1,744 @@
|
||||
{
|
||||
"com_a11y_ai_composing": "AI stále tvoří odpověď.",
|
||||
"com_a11y_end": "AI dokončila svou odpověď.",
|
||||
"com_a11y_start": "AI začala tvořit odpověď.",
|
||||
"com_agents_allow_editing": "Povolit ostatním uživatelům upravovat vašeho agenta",
|
||||
"com_agents_by_librechat": "od LibreChat",
|
||||
"com_agents_code_interpreter": "Při povolení může váš agent využívat LibreChat Code Interpreter API ke spuštění generovaného kódu, včetně zpracování souborů, bezpečně. Vyžaduje platný API klíč.",
|
||||
"com_agents_code_interpreter_title": "API pro interpretaci kódu",
|
||||
"com_agents_create_error": "Při vytváření agenta došlo k chybě.",
|
||||
"com_agents_description_placeholder": "Volitelné: Popište zde svého agenta",
|
||||
"com_agents_enable_file_search": "Povolit vyhledávání souborů",
|
||||
"com_agents_file_search_disabled": "Než nahrajete soubory pro vyhledávání, musíte vytvořit agenta.",
|
||||
"com_agents_file_search_info": "Při povolení bude agent informován o přesných názvech souborů uvedených níže, což mu umožní získat relevantní kontext z těchto souborů.",
|
||||
"com_agents_instructions_placeholder": "Systémové instrukce, které agent používá",
|
||||
"com_agents_missing_provider_model": "Před vytvořením agenta vyberte poskytovatele a model.",
|
||||
"com_agents_name_placeholder": "Volitelné: Název agenta",
|
||||
"com_agents_no_access": "Nemáte oprávnění upravovat tohoto agenta.",
|
||||
"com_agents_not_available": "Agent není k dispozici",
|
||||
"com_agents_search_name": "Hledat agenty podle jména",
|
||||
"com_agents_update_error": "Při aktualizaci agenta došlo k chybě.",
|
||||
"com_assistants_action_attempt": "Asistent se chce spojit s {{0}}",
|
||||
"com_assistants_actions": "Akce",
|
||||
"com_assistants_actions_disabled": "Než přidáte akce, musíte vytvořit asistenta.",
|
||||
"com_assistants_actions_info": "Umožněte asistentovi získávat informace nebo provádět akce přes API",
|
||||
"com_assistants_add_actions": "Přidat akce",
|
||||
"com_assistants_add_tools": "Přidat nástroje",
|
||||
"com_assistants_allow_sites_you_trust": "Povolte pouze weby, kterým důvěřujete.",
|
||||
"com_assistants_append_date": "Připojit aktuální datum a čas",
|
||||
"com_assistants_append_date_tooltip": "Při povolení se k systémovým instrukcím asistenta připojí aktuální datum a čas klienta.",
|
||||
"com_assistants_attempt_info": "Asistent chce odeslat následující:",
|
||||
"com_assistants_available_actions": "Dostupné akce",
|
||||
"com_assistants_capabilities": "Schopnosti",
|
||||
"com_assistants_code_interpreter": "Interpret kódu",
|
||||
"com_assistants_code_interpreter_files": "Soubory níže jsou určeny pouze pro interpret kódu:",
|
||||
"com_assistants_code_interpreter_info": "Interpret kódu umožňuje asistentovi psát a spouštět kód. Tento nástroj dokáže zpracovat soubory s různými daty a formáty a generovat soubory, například grafy.",
|
||||
"com_assistants_completed_action": "Spojil se s {{0}}",
|
||||
"com_assistants_completed_function": "Spustil {{0}}",
|
||||
"com_assistants_conversation_starters": "Témata konverzace",
|
||||
"com_assistants_conversation_starters_placeholder": "Zadejte téma konverzace",
|
||||
"com_assistants_create_error": "Při vytváření asistenta došlo k chybě.",
|
||||
"com_assistants_create_success": "Úspěšně vytvořeno",
|
||||
"com_assistants_delete_actions_error": "Při mazání akce došlo k chybě.",
|
||||
"com_assistants_delete_actions_success": "Akce byla úspěšně odstraněna z asistenta",
|
||||
"com_assistants_description_placeholder": "Volitelné: Popište zde svého asistenta",
|
||||
"com_assistants_domain_info": "Asistent poslal tyto informace: {{0}}",
|
||||
"com_assistants_file_search": "Vyhledávání souborů",
|
||||
"com_assistants_file_search_info": "Vyhledávání souborů umožňuje asistentovi pracovat se znalostmi z nahraných souborů. Jakmile je soubor nahrán, asistent automaticky rozhodne, kdy získat jeho obsah na základě požadavků uživatelů. Připojování vektorových úložišť pro vyhledávání není zatím podporováno.",
|
||||
"com_auth_already_have_account": "Už máte účet?",
|
||||
"com_auth_apple_login": "Přihlásit se přes Apple",
|
||||
"com_auth_back_to_login": "Zpět k přihlášení",
|
||||
"com_auth_click": "Klikněte",
|
||||
"com_auth_click_here": "Klikněte zde",
|
||||
"com_auth_continue": "Pokračovat",
|
||||
"com_auth_create_account": "Vytvořit účet",
|
||||
"com_auth_discord_login": "Pokračovat přes Discord",
|
||||
"com_auth_email": "E-mail",
|
||||
"com_auth_email_address": "E-mailová adresa",
|
||||
"com_auth_email_verification_invalid": "Neplatné ověření e-mailu",
|
||||
"com_auth_email_verification_redirecting": "Přesměrování za {{0}} sekund...",
|
||||
"com_auth_email_verification_resend_prompt": "Nedostali jste e-mail?",
|
||||
"com_auth_email_verification_success": "E-mail byl úspěšně ověřen",
|
||||
"com_auth_email_verifying_ellipsis": "Ověřování...",
|
||||
"com_auth_error_create": "Při registraci účtu došlo k chybě. Zkuste to prosím znovu.",
|
||||
"com_auth_error_invalid_reset_token": "Tento resetovací token hesla již není platný.",
|
||||
"com_auth_error_login": "Nelze se přihlásit s poskytnutými údaji. Zkontrolujte své přihlašovací údaje a zkuste to znovu.",
|
||||
"com_auth_error_login_ban": "Váš účet byl dočasně zablokován kvůli porušení našich pravidel.",
|
||||
"com_auth_error_login_rl": "Příliš mnoho pokusů o přihlášení v krátkém čase. Zkuste to prosím později.",
|
||||
"com_auth_error_login_server": "Došlo k interní chybě serveru. Počkejte několik okamžiků a zkuste to znovu.",
|
||||
"com_auth_error_login_unverified": "Váš účet nebyl ověřen. Zkontrolujte svůj e-mail a najděte ověřovací odkaz.",
|
||||
"com_auth_facebook_login": "Pokračovat přes Facebook",
|
||||
"com_auth_full_name": "Celé jméno",
|
||||
"com_auth_github_login": "Pokračovat přes Github",
|
||||
"com_auth_google_login": "Pokračovat přes Google",
|
||||
"com_auth_here": "ZDE",
|
||||
"com_auth_login": "Přihlášení",
|
||||
"com_auth_login_with_new_password": "Nyní se můžete přihlásit s novým heslem.",
|
||||
"com_auth_name_max_length": "Jméno musí mít méně než 80 znaků",
|
||||
"com_auth_name_min_length": "Jméno musí mít alespoň 3 znaky",
|
||||
"com_auth_name_required": "Jméno je povinné",
|
||||
"com_auth_no_account": "Nemáte účet?",
|
||||
"com_auth_password": "Heslo",
|
||||
"com_auth_password_confirm": "Potvrdit heslo",
|
||||
"com_auth_password_forgot": "Zapomněli jste heslo?",
|
||||
"com_auth_password_max_length": "Heslo musí mít méně než 128 znaků",
|
||||
"com_auth_password_min_length": "Heslo musí mít alespoň 8 znaků",
|
||||
"com_auth_password_not_match": "Hesla se neshodují",
|
||||
"com_auth_password_required": "Heslo je povinné",
|
||||
"com_auth_registration_success_generic": "Zkontrolujte svůj e-mail pro ověření vaší e-mailové adresy.",
|
||||
"com_auth_registration_success_insecure": "Registrace byla úspěšná.",
|
||||
"com_auth_reset_password": "Obnovit heslo",
|
||||
"com_auth_reset_password_if_email_exists": "Pokud účet s touto e-mailovou adresou existuje, byl odeslán e-mail s instrukcemi pro resetování hesla. Nezapomeňte zkontrolovat složku spamu.",
|
||||
"com_auth_reset_password_link_sent": "E-mail odeslán",
|
||||
"com_auth_reset_password_success": "Obnova hesla úspěšná",
|
||||
"com_auth_sign_in": "Přihlásit se",
|
||||
"com_auth_sign_up": "Registrovat se",
|
||||
"com_auth_submit_registration": "Odeslat registraci",
|
||||
"com_auth_to_reset_your_password": "pro obnovení hesla.",
|
||||
"com_auth_to_try_again": "zkusit znovu.",
|
||||
"com_auth_two_factor": "Zkontrolujte vaši preferovanou aplikaci pro jednorázové heslo a zadejte kód",
|
||||
"com_auth_username": "Uživatelské jméno (volitelné)",
|
||||
"com_auth_username_max_length": "Uživatelské jméno musí mít méně než 20 znaků",
|
||||
"com_auth_username_min_length": "Uživatelské jméno musí mít alespoň 2 znaky",
|
||||
"com_auth_verify_your_identity": "Ověřte svou identitu",
|
||||
"com_auth_welcome_back": "Vítejte zpět",
|
||||
"com_click_to_download": "(klikněte zde pro stažení)",
|
||||
"com_download_expired": "(platnost stažení vypršela)",
|
||||
"com_download_expires": "(klikněte zde pro stažení - platnost vyprší {{0}})",
|
||||
"com_endpoint": "Koncový bod",
|
||||
"com_endpoint_agent": "Agent",
|
||||
"com_endpoint_agent_model": "Model agenta (Doporučeno: GPT-3.5)",
|
||||
"com_endpoint_agent_placeholder": "Vyberte prosím agenta",
|
||||
"com_endpoint_ai": "AI",
|
||||
"com_endpoint_anthropic_maxoutputtokens": "Maximální počet tokenů, které lze v odpovědi vygenerovat. Zadejte nižší hodnotu pro kratší odpovědi a vyšší hodnotu pro delší odpovědi. Poznámka: modely mohou skončit před dosažením tohoto maxima.",
|
||||
"com_endpoint_anthropic_prompt_cache": "Ukládání výzev umožňuje znovu použít rozsáhlý kontext nebo instrukce napříč API voláními, čímž se snižují náklady a latence.",
|
||||
"com_endpoint_anthropic_temp": "Hodnoty od 0 do 1. Použijte hodnotu blíže k 0 pro analytické / výběrové úlohy a blíže k 1 pro kreativní a generativní úkoly.",
|
||||
"com_endpoint_anthropic_thinking": "Povoluje interní uvažování u podporovaných modelů Claude (3.7 Sonnet). Poznámka: vyžaduje nastavení \"Thinking Budget\" na nižší hodnotu než \"Max Output Tokens\".",
|
||||
"com_endpoint_anthropic_thinking_budget": "Určuje maximální počet tokenů, které může Claude použít pro svůj interní proces uvažování.",
|
||||
"com_endpoint_assistant": "Asistent",
|
||||
"com_endpoint_assistant_model": "Model asistenta",
|
||||
"com_endpoint_completion": "Dokončení",
|
||||
"com_endpoint_completion_model": "Model dokončení (Doporučeno: GPT-4)",
|
||||
"com_endpoint_config_key_name": "Klíč",
|
||||
"com_endpoint_config_key_never_expires": "Váš klíč nikdy nevyprší",
|
||||
"com_endpoint_config_placeholder": "Nastavte svůj klíč v nabídce záhlaví pro chat.",
|
||||
"com_endpoint_config_value": "Zadejte hodnotu pro",
|
||||
"com_endpoint_context": "Kontext",
|
||||
"com_endpoint_context_info": "Maximální počet tokenů, které lze použít pro kontext. Použijte toto pro kontrolu počtu tokenů odeslaných na požadavek. Pokud není uvedeno, použije se výchozí nastavení systému podle velikosti kontextu známých modelů. Nastavení vyšších hodnot může vést k chybám a/nebo vyšším nákladům na tokeny.",
|
||||
"com_endpoint_context_tokens": "Maximální počet kontextových tokenů",
|
||||
"com_endpoint_custom_name": "Vlastní název",
|
||||
"com_endpoint_default": "výchozí",
|
||||
"com_endpoint_default_blank": "výchozí: prázdné",
|
||||
"com_endpoint_default_empty": "výchozí: prázdné",
|
||||
"com_endpoint_default_with_num": "výchozí: {{0}}",
|
||||
"com_endpoint_examples": "Předvolby",
|
||||
"com_endpoint_export": "Exportovat",
|
||||
"com_endpoint_export_share": "Exportovat/Sdílet",
|
||||
"com_endpoint_frequency_penalty": "Postih za časté opakování",
|
||||
"com_endpoint_func_hover": "Povolit použití pluginů jako funkcí OpenAI",
|
||||
"com_endpoint_google_custom_name_placeholder": "Nastavit vlastní název pro Google",
|
||||
"com_endpoint_google_maxoutputtokens": "Maximální počet tokenů, které lze v odpovědi vygenerovat. Nižší hodnota pro kratší odpovědi, vyšší hodnota pro delší odpovědi. Poznámka: modely mohou skončit před dosažením tohoto maxima.",
|
||||
"com_endpoint_google_temp": "Vyšší hodnoty = větší náhodnost, nižší hodnoty = soustředěnější a deterministický výstup. Doporučujeme upravit buď toto, nebo Top P, ale ne obojí.",
|
||||
"com_endpoint_google_topk": "Top-k mění způsob, jakým model vybírá tokeny pro výstup. Top-k 1 znamená, že vybraný token je nejpravděpodobnější ze všech v modelové slovní zásobě (také nazývané chamtivé dekódování), zatímco top-k 3 znamená, že další token je vybrán ze tří nejpravděpodobnějších (s použitím teploty).",
|
||||
"com_endpoint_google_topp": "Top-p mění způsob, jakým model vybírá tokeny pro výstup. Tokeny jsou vybírány od nejpravděpodobnějších, dokud součet jejich pravděpodobností nedosáhne hodnoty top-p.",
|
||||
"com_endpoint_instructions_assistants": "Přepsat instrukce",
|
||||
"com_endpoint_instructions_assistants_placeholder": "Přepíše instrukce asistenta. Užitečné pro úpravu chování v jednotlivých bězích.",
|
||||
"com_endpoint_max_output_tokens": "Maximální počet výstupních tokenů",
|
||||
"com_endpoint_message": "Zpráva",
|
||||
"com_endpoint_message_new": "Zpráva {{0}}",
|
||||
"com_endpoint_message_not_appendable": "Upravte svou zprávu nebo ji znovu vygenerujte.",
|
||||
"com_endpoint_my_preset": "Moje předvolba",
|
||||
"com_endpoint_no_presets": "Žádné předvolby zatím nejsou, použijte tlačítko nastavení k vytvoření jedné",
|
||||
"com_endpoint_open_menu": "Otevřít nabídku",
|
||||
"com_endpoint_openai_custom_name_placeholder": "Nastavit vlastní název pro AI",
|
||||
"com_endpoint_openai_temp": "Vyšší hodnoty = větší náhodnost, nižší hodnoty = soustředěnější a deterministický výstup.",
|
||||
"com_endpoint_openai_topp": "Alternativa k vzorkování s teplotou, tzv. nucleus sampling, kde model zvažuje výsledky tokenů s nejvyšší pravděpodobností. Například hodnota 0.1 znamená, že se berou v úvahu pouze tokeny tvořící 10 % nejvyšší pravděpodobnosti.",
|
||||
"com_endpoint_output": "Výstup",
|
||||
"com_endpoint_plug_image_detail": "Detail obrazu",
|
||||
"com_endpoint_plug_resend_files": "Znovu odeslat soubory",
|
||||
"com_endpoint_prompt_cache": "Použít cache výzev",
|
||||
"com_endpoint_prompt_prefix": "Vlastní instrukce",
|
||||
"com_endpoint_prompt_prefix_assistants": "Další instrukce",
|
||||
"com_endpoint_reasoning_effort": "Úroveň úsilí při uvažování",
|
||||
"com_endpoint_save_as_preset": "Uložit jako předvolbu",
|
||||
"com_endpoint_search": "Hledat koncový bod podle názvu",
|
||||
"com_endpoint_stop": "Zastavit sekvence",
|
||||
"com_endpoint_temperature": "Teplota",
|
||||
"com_endpoint_thinking": "Přemýšlení",
|
||||
"com_endpoint_thinking_budget": "Rozpočet na přemýšlení",
|
||||
"com_endpoint_top_k": "Top K",
|
||||
"com_endpoint_top_p": "Top P",
|
||||
"com_endpoint_use_active_assistant": "Použít aktivního asistenta",
|
||||
"com_error_expired_user_key": "Poskytnutý klíč pro {{0}} vypršel v {{1}}. Zadejte nový klíč a zkuste to znovu.",
|
||||
"com_error_files_dupe": "Byl zjištěn duplicitní soubor.",
|
||||
"com_error_files_empty": "Prázdné soubory nejsou povoleny.",
|
||||
"com_error_files_process": "Při zpracování souboru došlo k chybě.",
|
||||
"com_error_files_unsupported_capability": "Nejsou povoleny žádné funkce podporující tento typ souboru.",
|
||||
"com_error_files_upload": "Při nahrávání souboru došlo k chybě.",
|
||||
"com_error_files_upload_canceled": "Požadavek na nahrání souboru byl zrušen. Poznámka: nahrávání souboru může stále probíhat a bude nutné jej ručně smazat.",
|
||||
"com_error_files_validation": "Při ověřování souboru došlo k chybě.",
|
||||
"com_error_input_length": "Počet tokenů v poslední zprávě je příliš dlouhý a přesahuje limit tokenů ({{0}}). Zkraťte svou zprávu, upravte maximální velikost kontextu v parametrech konverzace nebo rozdělte konverzaci.",
|
||||
"com_error_invalid_user_key": "Zadaný klíč je neplatný. Zadejte platný klíč a zkuste to znovu.",
|
||||
"com_error_moderation": "Zdá se, že obsah vaší zprávy byl označen naším moderovacím systémem, protože neodpovídá našim komunitním zásadám. Nemůžeme v této záležitosti pokračovat. Pokud máte jiné otázky nebo témata, která chcete prozkoumat, upravte svou zprávu nebo vytvořte novou konverzaci.",
|
||||
"com_error_no_base_url": "Nebyla nalezena základní URL. Zadejte ji a zkuste to znovu.",
|
||||
"com_error_no_user_key": "Nebyl nalezen žádný klíč. Zadejte klíč a zkuste to znovu.",
|
||||
"com_files_filter": "Filtrovat soubory...",
|
||||
"com_files_no_results": "Žádné výsledky.",
|
||||
"com_files_number_selected": "Vybráno {{0}} z {{1}} položek",
|
||||
"com_files_table": "něco sem musí přijít. bylo prázdné",
|
||||
"com_generated_files": "Vygenerované soubory:",
|
||||
"com_hide_examples": "Skrýt příklady",
|
||||
"com_nav_2fa": "Dvoufaktorové ověřování (2FA)",
|
||||
"com_nav_account_settings": "Nastavení účtu",
|
||||
"com_nav_always_make_prod": "Vždy nastavovat nové verze jako produkční",
|
||||
"com_nav_archive_created_at": "Datum archivace",
|
||||
"com_nav_archive_name": "Název",
|
||||
"com_nav_archived_chats": "Archivované chaty",
|
||||
"com_nav_archived_chats_empty": "Nemáte žádné archivované konverzace.",
|
||||
"com_nav_at_command": "@-Příkaz",
|
||||
"com_nav_at_command_description": "Přepínání příkazů \"@\" pro přepínání koncových bodů, modelů, předvoleb atd.",
|
||||
"com_nav_audio_play_error": "Chyba při přehrávání zvuku: {{0}}",
|
||||
"com_nav_audio_process_error": "Chyba při zpracování zvuku: {{0}}",
|
||||
"com_nav_auto_scroll": "Automaticky rolovat na nejnovější zprávu po otevření chatu",
|
||||
"com_nav_auto_send_prompts": "Automatické odesílání výzev",
|
||||
"com_nav_auto_send_text": "Automatické odesílání textu",
|
||||
"com_nav_auto_send_text_disabled": "nastavte -1 pro deaktivaci",
|
||||
"com_nav_auto_transcribe_audio": "Automaticky přepisovat zvuk",
|
||||
"com_nav_automatic_playback": "Automatické přehrávání poslední zprávy",
|
||||
"com_nav_balance": "Zůstatek",
|
||||
"com_nav_browser": "Prohlížeč",
|
||||
"com_nav_buffer_append_error": "Problém s přenosem zvuku. Přehrávání může být přerušeno.",
|
||||
"com_nav_change_picture": "Změnit obrázek",
|
||||
"com_nav_chat_commands": "Příkazy chatu",
|
||||
"com_nav_chat_commands_info": "Tyto příkazy se aktivují zadáním specifických znaků na začátku vaší zprávy. Každý příkaz je spuštěn svým určeným prefixem. Můžete je deaktivovat, pokud tyto znaky často používáte na začátku zpráv.",
|
||||
"com_nav_chat_direction": "Směr chatu",
|
||||
"com_nav_clear_all_chats": "Vymazat všechny chaty",
|
||||
"com_nav_clear_cache_confirm_message": "Opravdu chcete vymazat mezipaměť?",
|
||||
"com_nav_clear_conversation": "Vymazat konverzace",
|
||||
"com_nav_clear_conversation_confirm_message": "Opravdu chcete vymazat všechny konverzace? Tuto akci nelze vrátit zpět.",
|
||||
"com_nav_close_sidebar": "Zavřít boční panel",
|
||||
"com_nav_commands": "Příkazy",
|
||||
"com_nav_confirm_clear": "Potvrdit vymazání",
|
||||
"com_nav_conversation_mode": "Režim konverzace",
|
||||
"com_nav_convo_menu_options": "Možnosti menu konverzace",
|
||||
"com_nav_db_sensitivity": "Citlivost decibelů",
|
||||
"com_nav_delete_account": "Smazat účet",
|
||||
"com_nav_delete_account_button": "Trvale smazat můj účet",
|
||||
"com_nav_delete_account_confirm": "Smazat účet - jste si jisti?",
|
||||
"com_nav_delete_account_email_placeholder": "Zadejte e-mail vašeho účtu",
|
||||
"com_nav_delete_cache_storage": "Smazat úložiště mezipaměti TTS",
|
||||
"com_nav_delete_data_info": "Všechna vaše data budou smazána.",
|
||||
"com_nav_delete_warning": "VAROVÁNÍ: Tato akce trvale smaže váš účet.",
|
||||
"com_nav_edge": "Edge",
|
||||
"com_nav_enable_cache_tts": "Povolit mezipaměť TTS",
|
||||
"com_nav_enable_cloud_browser_voice": "Používat cloudové hlasy",
|
||||
"com_nav_enabled": "Povoleno",
|
||||
"com_nav_engine": "Engine",
|
||||
"com_nav_enter_to_send": "Stiskněte Enter pro odeslání zprávy",
|
||||
"com_nav_export": "Exportovat",
|
||||
"com_nav_export_all_message_branches": "Exportovat všechny větve zpráv",
|
||||
"com_nav_export_conversation": "Exportovat konverzaci",
|
||||
"com_nav_export_filename": "Název souboru",
|
||||
"com_nav_export_filename_placeholder": "Zadejte název souboru",
|
||||
"com_nav_export_include_endpoint_options": "Zahrnout možnosti koncového bodu",
|
||||
"com_nav_export_recursive": "Rekurzivní",
|
||||
"com_nav_export_recursive_or_sequential": "Rekurzivní nebo sekvenční?",
|
||||
"com_nav_export_type": "Typ",
|
||||
"com_nav_external": "Externí",
|
||||
"com_nav_font_size": "Velikost písma zprávy",
|
||||
"com_nav_font_size_base": "Střední",
|
||||
"com_nav_font_size_lg": "Velká",
|
||||
"com_nav_font_size_sm": "Malá",
|
||||
"com_nav_font_size_xl": "Extra velká",
|
||||
"com_nav_font_size_xs": "Extra malá",
|
||||
"com_nav_help_faq": "Nápověda a FAQ",
|
||||
"com_nav_hide_panel": "Skrýt pravý panel",
|
||||
"com_nav_info_code_artifacts": "Povoluje zobrazování experimentálních kódových artefaktů vedle chatu",
|
||||
"com_nav_info_code_artifacts_agent": "Povoluje použití kódových artefaktů pro tohoto agenta. Ve výchozím nastavení jsou přidány další instrukce specifické pro použití artefaktů, pokud není povolen režim \"Vlastní výzva\".",
|
||||
"com_nav_info_custom_prompt_mode": "Při povolení nebude zahrnuta výchozí systémová výzva pro artefakty. Všechny instrukce pro generování artefaktů musí být v tomto režimu poskytnuty ručně.",
|
||||
"com_nav_info_enter_to_send": "Při povolení odešle stisk `ENTER` zprávu. Při deaktivaci přidá Enter nový řádek a zprávu odešlete stiskem `CTRL + ENTER` / `⌘ + ENTER`.",
|
||||
"com_nav_info_fork_change_default": "`Viditelné zprávy pouze` zahrnuje pouze přímou cestu k vybrané zprávě. `Zahrnout související větve` přidá větve podél cesty. `Zahrnout vše od/do` zahrnuje všechny propojené zprávy a větve.",
|
||||
"com_nav_info_fork_split_target_setting": "Při povolení začne větvení od cílové zprávy až po nejnovější zprávu v konverzaci podle zvoleného chování.",
|
||||
"com_nav_info_include_shadcnui": "Při povolení budou zahrnuty instrukce pro použití komponent shadcn/ui.",
|
||||
"com_nav_info_latex_parsing": "Při povolení bude LaTeX kód v zprávách vykreslen jako matematické rovnice.",
|
||||
"com_nav_info_save_draft": "Při povolení se text a přílohy, které zadáte do chatu, automaticky ukládají jako koncepty.",
|
||||
"com_nav_info_show_thinking": "Při povolení se automaticky zobrazí rozbalovací nabídky uvažování AI.",
|
||||
"com_nav_info_user_name_display": "Při povolení se nad každou vaší zprávou zobrazí uživatelské jméno.",
|
||||
"com_nav_lang_arabic": "العربية",
|
||||
"com_nav_lang_auto": "Automatické rozpoznání",
|
||||
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
|
||||
"com_nav_lang_chinese": "中文",
|
||||
"com_nav_lang_dutch": "Nederlands",
|
||||
"com_nav_lang_english": "English",
|
||||
"com_nav_lang_french": "Français ",
|
||||
"com_nav_lang_german": "Deutsch",
|
||||
"com_nav_lang_italian": "Italiano",
|
||||
"com_nav_lang_japanese": "日本語",
|
||||
"com_nav_lang_korean": "한국어",
|
||||
"com_nav_lang_polish": "Polski",
|
||||
"com_nav_lang_portuguese": "Português",
|
||||
"com_nav_lang_russian": "Русский",
|
||||
"com_nav_lang_spanish": "Español",
|
||||
"com_nav_lang_swedish": "Svenska",
|
||||
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||
"com_nav_lang_turkish": "Türkçe",
|
||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||
"com_nav_language": "Jazyk",
|
||||
"com_nav_latex_parsing": "Zpracování LaTeXu v zprávách",
|
||||
"com_nav_log_out": "Odhlásit se",
|
||||
"com_nav_maximize_chat_space": "Maximalizovat prostor chatu",
|
||||
"com_nav_modular_chat": "Povolit přepínání koncových bodů během konverzace",
|
||||
"com_nav_my_files": "Moje soubory",
|
||||
"com_nav_no_search_results": "Nebyl nalezen žádný výsledek",
|
||||
"com_nav_not_supported": "Nepodporováno",
|
||||
"com_nav_open_sidebar": "Otevřít boční panel",
|
||||
"com_nav_playback_rate": "Rychlost přehrávání zvuku",
|
||||
"com_nav_plugin_auth_error": "Při ověřování pluginu došlo k chybě.",
|
||||
"com_nav_plugin_install": "Instalovat",
|
||||
"com_nav_plugin_search": "Hledat pluginy",
|
||||
"com_nav_plugin_store": "Obchod s pluginy",
|
||||
"com_nav_plugin_uninstall": "Odinstalovat",
|
||||
"com_nav_plus_command": "+-Příkaz",
|
||||
"com_nav_plus_command_description": "Přepnutí příkazu \"+\" pro přidání nastavení více odpovědí",
|
||||
"com_nav_profile_picture": "Profilový obrázek",
|
||||
"com_nav_save_drafts": "Ukládat koncepty lokálně",
|
||||
"com_nav_scroll_button": "Tlačítko pro posun na konec",
|
||||
"com_nav_search_placeholder": "Hledat zprávy",
|
||||
"com_nav_send_message": "Odeslat zprávu",
|
||||
"com_nav_setting_account": "Účet",
|
||||
"com_nav_setting_beta": "Beta funkce",
|
||||
"com_nav_setting_chat": "Chat",
|
||||
"com_nav_setting_data": "Ovládání dat",
|
||||
"com_nav_setting_general": "Obecné",
|
||||
"com_nav_setting_speech": "Hlas",
|
||||
"com_nav_settings": "Nastavení",
|
||||
"com_nav_shared_links": "Sdílené odkazy",
|
||||
"com_nav_show_code": "Vždy zobrazit kód při použití interpretace kódu",
|
||||
"com_nav_show_thinking": "Otevřít uvažovací nabídky ve výchozím nastavení",
|
||||
"com_nav_slash_command": "/-Příkaz",
|
||||
"com_nav_slash_command_description": "Přepnutí příkazu \"/\" pro výběr výzvy pomocí klávesnice",
|
||||
"com_nav_source_buffer_error": "Chyba při nastavení přehrávání zvuku. Obnovte stránku.",
|
||||
"com_nav_speech_cancel_error": "Nelze zastavit přehrávání zvuku. Možná budete muset stránku obnovit.",
|
||||
"com_nav_speech_to_text": "Převod řeči na text",
|
||||
"com_nav_stop_generating": "Zastavit generování",
|
||||
"com_nav_text_to_speech": "Převod textu na řeč",
|
||||
"com_nav_theme": "Motiv",
|
||||
"com_nav_theme_dark": "Tmavý",
|
||||
"com_nav_theme_light": "Světlý",
|
||||
"com_nav_theme_system": "Systémový",
|
||||
"com_nav_tool_dialog": "Nástroje asistenta",
|
||||
"com_nav_tool_dialog_agents": "Nástroje agenta",
|
||||
"com_nav_tool_dialog_description": "Asistent musí být uložen, aby výběr nástrojů přetrval.",
|
||||
"com_nav_tool_remove": "Odstranit",
|
||||
"com_nav_tool_search": "Hledat nástroje",
|
||||
"com_nav_tts_init_error": "Nepodařilo se inicializovat převod textu na řeč: {{0}}",
|
||||
"com_nav_tts_unsupported_error": "Převod textu na řeč pro vybraný engine není v tomto prohlížeči podporován.",
|
||||
"com_nav_user": "UŽIVATEL",
|
||||
"com_nav_user_msg_markdown": "Zobrazit uživatelské zprávy ve formátu Markdown",
|
||||
"com_nav_user_name_display": "Zobrazit uživatelské jméno ve zprávách",
|
||||
"com_nav_voice_select": "Hlas",
|
||||
"com_nav_voices_fetch_error": "Nepodařilo se načíst možnosti hlasu. Zkontrolujte připojení k internetu.",
|
||||
"com_nav_welcome_agent": "Vyberte agenta",
|
||||
"com_nav_welcome_assistant": "Vyberte asistenta",
|
||||
"com_nav_welcome_message": "Jak vám mohu dnes pomoci?",
|
||||
"com_show_agent_settings": "Zobrazit nastavení agenta",
|
||||
"com_show_completion_settings": "Zobrazit nastavení dokončení",
|
||||
"com_show_examples": "Zobrazit příklady",
|
||||
"com_sidepanel_agent_builder": "Tvůrce agentů",
|
||||
"com_sidepanel_assistant_builder": "Tvůrce asistentů",
|
||||
"com_sidepanel_attach_files": "Připojit soubory",
|
||||
"com_sidepanel_conversation_tags": "Záložky",
|
||||
"com_sidepanel_hide_panel": "Skrýt panel",
|
||||
"com_sidepanel_manage_files": "Správa souborů",
|
||||
"com_sidepanel_parameters": "Parametry",
|
||||
"com_sidepanel_select_agent": "Vybrat agenta",
|
||||
"com_sidepanel_select_assistant": "Vybrat asistenta",
|
||||
"com_ui_2fa_account_security": "Dvoufaktorové ověřování přidává další vrstvu zabezpečení vašeho účtu",
|
||||
"com_ui_2fa_disable": "Zakázat 2FA",
|
||||
"com_ui_2fa_disable_error": "Při deaktivaci dvoufaktorového ověřování došlo k chybě",
|
||||
"com_ui_2fa_disabled": "2FA bylo deaktivováno",
|
||||
"com_ui_2fa_enable": "Povolit 2FA",
|
||||
"com_ui_2fa_enabled": "2FA bylo povoleno",
|
||||
"com_ui_2fa_generate_error": "Při generování nastavení 2FA došlo k chybě",
|
||||
"com_ui_2fa_invalid": "Neplatný kód dvoufaktorového ověřování",
|
||||
"com_ui_2fa_setup": "Nastavit 2FA",
|
||||
"com_ui_2fa_verified": "Dvoufaktorové ověřování úspěšně ověřeno",
|
||||
"com_ui_accept": "Přijímám",
|
||||
"com_ui_add": "Přidat",
|
||||
"com_ui_add_model_preset": "Přidat model nebo předvolbu pro další odpověď",
|
||||
"com_ui_add_multi_conversation": "Přidat více konverzací",
|
||||
"com_ui_admin": "Administrátor",
|
||||
"com_ui_admin_access_warning": "Zakázání přístupu správce k této funkci může způsobit problémy v uživatelském rozhraní.",
|
||||
"com_ui_admin_settings": "Nastavení správce",
|
||||
"com_ui_advanced": "Pokročilé",
|
||||
"com_ui_agent": "Agent",
|
||||
"com_ui_agent_delete_error": "Při mazání agenta došlo k chybě",
|
||||
"com_ui_agent_deleted": "Agent byl úspěšně smazán",
|
||||
"com_ui_agent_duplicate_error": "Při duplikaci agenta došlo k chybě",
|
||||
"com_ui_agent_duplicated": "Agent byl úspěšně duplikován",
|
||||
"com_ui_agents": "Agenti",
|
||||
"com_ui_agents_allow_create": "Povolit vytváření agentů",
|
||||
"com_ui_agents_allow_share_global": "Povolit sdílení agentů všem uživatelům",
|
||||
"com_ui_agents_allow_use": "Povolit používání agentů",
|
||||
"com_ui_all": "vše",
|
||||
"com_ui_all_proper": "Vše",
|
||||
"com_ui_analyzing": "Analýza",
|
||||
"com_ui_analyzing_finished": "Analýza dokončena",
|
||||
"com_ui_api_key": "API klíč",
|
||||
"com_ui_archive": "Archivovat",
|
||||
"com_ui_archive_error": "Nepodařilo se archivovat konverzaci",
|
||||
"com_ui_artifact_click": "Klikněte pro otevření",
|
||||
"com_ui_artifacts": "Artefakty",
|
||||
"com_ui_artifacts_toggle": "Přepnout uživatelské rozhraní artefaktů",
|
||||
"com_ui_artifacts_toggle_agent": "Povolit artefakty",
|
||||
"com_ui_ascending": "Vzestupně",
|
||||
"com_ui_assistant": "Asistent",
|
||||
"com_ui_assistant_delete_error": "Při mazání asistenta došlo k chybě",
|
||||
"com_ui_assistant_deleted": "Asistent byl úspěšně smazán",
|
||||
"com_ui_assistants": "Asistenti",
|
||||
"com_ui_assistants_output": "Výstup asistentů",
|
||||
"com_ui_attach_error": "Nelze připojit soubor. Vytvořte nebo vyberte konverzaci.",
|
||||
"com_ui_attach_error_openai": "Nelze připojit soubory asistenta k jiným koncovým bodům",
|
||||
"com_ui_attach_error_size": "Překročena velikost souboru pro koncový bod:",
|
||||
"com_ui_attach_error_type": "Nepodporovaný typ souboru pro koncový bod:",
|
||||
"com_ui_attach_warn_endpoint": "Nepodporované soubory mohou být ignorovány",
|
||||
"com_ui_attachment": "Příloha",
|
||||
"com_ui_auth_type": "Typ ověření",
|
||||
"com_ui_auth_url": "Autorizační URL",
|
||||
"com_ui_authentication": "Ověření",
|
||||
"com_ui_authentication_type": "Typ ověření",
|
||||
"com_ui_avatar": "Avatar",
|
||||
"com_ui_azure": "Azure",
|
||||
"com_ui_back_to_chat": "Zpět do chatu",
|
||||
"com_ui_back_to_prompts": "Zpět na výzvy",
|
||||
"com_ui_backup_codes": "Záložní kódy",
|
||||
"com_ui_backup_codes_regenerate_error": "Při generování záložních kódů došlo k chybě",
|
||||
"com_ui_backup_codes_regenerated": "Záložní kódy byly úspěšně vygenerovány",
|
||||
"com_ui_basic": "Základní",
|
||||
"com_ui_basic_auth_header": "Základní autorizační hlavička",
|
||||
"com_ui_bearer": "Bearer",
|
||||
"com_ui_bookmark_delete_confirm": "Opravdu chcete smazat tuto záložku?",
|
||||
"com_ui_bookmarks": "Záložky",
|
||||
"com_ui_bookmarks_add": "Přidat záložky",
|
||||
"com_ui_bookmarks_add_to_conversation": "Přidat do aktuální konverzace",
|
||||
"com_ui_bookmarks_count": "Počet",
|
||||
"com_ui_bookmarks_create_error": "Při vytváření záložky došlo k chybě",
|
||||
"com_ui_bookmarks_create_exists": "Tato záložka již existuje",
|
||||
"com_ui_bookmarks_create_success": "Záložka byla úspěšně vytvořena",
|
||||
"com_ui_bookmarks_delete": "Smazat záložku",
|
||||
"com_ui_bookmarks_delete_error": "Při mazání záložky došlo k chybě",
|
||||
"com_ui_bookmarks_delete_success": "Záložka byla úspěšně smazána",
|
||||
"com_ui_bookmarks_description": "Popis",
|
||||
"com_ui_bookmarks_edit": "Upravit záložku",
|
||||
"com_ui_bookmarks_filter": "Filtrovat záložky...",
|
||||
"com_ui_bookmarks_new": "Nová záložka",
|
||||
"com_ui_bookmarks_title": "Název",
|
||||
"com_ui_bookmarks_update_error": "Při aktualizaci záložky došlo k chybě",
|
||||
"com_ui_bookmarks_update_success": "Záložka byla úspěšně aktualizována",
|
||||
"com_ui_bulk_delete_error": "Nepodařilo se smazat sdílené odkazy",
|
||||
"com_ui_callback_url": "Callback URL",
|
||||
"com_ui_cancel": "Zrušit",
|
||||
"com_ui_chat": "Chat",
|
||||
"com_ui_chat_history": "Historie chatu",
|
||||
"com_ui_clear": "Vymazat",
|
||||
"com_ui_clear_all": "Vymazat vše",
|
||||
"com_ui_client_id": "ID klienta",
|
||||
"com_ui_client_secret": "Tajný klíč klienta",
|
||||
"com_ui_close": "Zavřít",
|
||||
"com_ui_close_menu": "Zavřít nabídku",
|
||||
"com_ui_code": "Kód",
|
||||
"com_ui_collapse_chat": "Sbalit chat",
|
||||
"com_ui_command_placeholder": "Volitelné: Zadejte příkaz pro výzvu, jinak se použije název",
|
||||
"com_ui_command_usage_placeholder": "Vybrat výzvu podle příkazu nebo názvu",
|
||||
"com_ui_complete_setup": "Dokončit nastavení",
|
||||
"com_ui_confirm_action": "Potvrdit akci",
|
||||
"com_ui_confirm_admin_use_change": "Změna tohoto nastavení zablokuje přístup správcům, včetně vás. Opravdu chcete pokračovat?",
|
||||
"com_ui_confirm_change": "Potvrdit změnu",
|
||||
"com_ui_context": "Kontext",
|
||||
"com_ui_continue": "Pokračovat",
|
||||
"com_ui_controls": "Ovládání",
|
||||
"com_ui_copied": "Zkopírováno!",
|
||||
"com_ui_copied_to_clipboard": "Zkopírováno do schránky",
|
||||
"com_ui_copy_code": "Kopírovat kód",
|
||||
"com_ui_copy_link": "Kopírovat odkaz",
|
||||
"com_ui_copy_to_clipboard": "Kopírovat do schránky",
|
||||
"com_ui_create": "Vytvořit",
|
||||
"com_ui_create_link": "Vytvořit odkaz",
|
||||
"com_ui_create_prompt": "Vytvořit výzvu",
|
||||
"com_ui_currently_production": "Aktuálně ve výrobě",
|
||||
"com_ui_custom": "Vlastní",
|
||||
"com_ui_custom_header_name": "Vlastní název hlavičky",
|
||||
"com_ui_custom_prompt_mode": "Režim vlastní výzvy",
|
||||
"com_ui_dashboard": "Dashboard",
|
||||
"com_ui_date": "Datum",
|
||||
"com_ui_date_april": "Duben",
|
||||
"com_ui_date_august": "Srpen",
|
||||
"com_ui_date_december": "Prosinec",
|
||||
"com_ui_date_february": "Únor",
|
||||
"com_ui_date_january": "Leden",
|
||||
"com_ui_date_july": "Červenec",
|
||||
"com_ui_date_june": "Červen",
|
||||
"com_ui_date_march": "Březen",
|
||||
"com_ui_date_may": "Květen",
|
||||
"com_ui_date_november": "Listopad",
|
||||
"com_ui_date_october": "Říjen",
|
||||
"com_ui_date_previous_30_days": "Předchozích 30 dní",
|
||||
"com_ui_date_previous_7_days": "Předchozích 7 dní",
|
||||
"com_ui_date_september": "Září",
|
||||
"com_ui_date_today": "Dnes",
|
||||
"com_ui_date_yesterday": "Včera",
|
||||
"com_ui_decline": "Nepřijímám",
|
||||
"com_ui_default_post_request": "Výchozí (POST request)",
|
||||
"com_ui_delete": "Smazat",
|
||||
"com_ui_delete_action": "Smazat akci",
|
||||
"com_ui_delete_action_confirm": "Opravdu chcete tuto akci smazat?",
|
||||
"com_ui_delete_agent_confirm": "Opravdu chcete tohoto agenta smazat?",
|
||||
"com_ui_delete_assistant_confirm": "Opravdu chcete tohoto asistenta smazat? Tuto akci nelze vrátit zpět.",
|
||||
"com_ui_delete_confirm": "Tímto smažete",
|
||||
"com_ui_delete_confirm_prompt_version_var": "Tímto smažete vybranou verzi pro \"{{0}}.\" Pokud neexistují žádné další verze, výzva bude smazána.",
|
||||
"com_ui_delete_conversation": "Smazat chat?",
|
||||
"com_ui_delete_prompt": "Smazat výzvu?",
|
||||
"com_ui_delete_shared_link": "Smazat sdílený odkaz?",
|
||||
"com_ui_delete_tool": "Smazat nástroj",
|
||||
"com_ui_delete_tool_confirm": "Opravdu chcete tento nástroj smazat?",
|
||||
"com_ui_descending": "Sestupně",
|
||||
"com_ui_description": "Popis",
|
||||
"com_ui_description_placeholder": "Volitelné: Zadejte popis pro zobrazení výzvy",
|
||||
"com_ui_disabling": "Deaktivace...",
|
||||
"com_ui_download": "Stáhnout",
|
||||
"com_ui_download_artifact": "Stáhnout artefakt",
|
||||
"com_ui_download_backup": "Stáhnout záložní kódy",
|
||||
"com_ui_download_backup_tooltip": "Před pokračováním si stáhněte záložní kódy. Budete je potřebovat k opětovnému přístupu v případě ztráty autentizačního zařízení.",
|
||||
"com_ui_download_error": "Chyba při stahování souboru. Soubor mohl být smazán.",
|
||||
"com_ui_drag_drop": "něco sem musí přijít. bylo prázdné",
|
||||
"com_ui_dropdown_variables": "Proměnné rozevírací nabídky:",
|
||||
"com_ui_dropdown_variables_info": "Vytvořte vlastní rozevírací nabídky pro vaše výzvy: `{{variable_name:option1|option2|option3}}`",
|
||||
"com_ui_duplicate": "Duplikovat",
|
||||
"com_ui_duplication_error": "Při duplikaci konverzace došlo k chybě",
|
||||
"com_ui_duplication_processing": "Duplikuji konverzaci...",
|
||||
"com_ui_duplication_success": "Konverzace úspěšně duplikována",
|
||||
"com_ui_edit": "Upravit",
|
||||
"com_ui_empty_category": "-",
|
||||
"com_ui_endpoint": "Koncový bod",
|
||||
"com_ui_endpoint_menu": "Nabídka LLM koncových bodů",
|
||||
"com_ui_endpoints_available": "Dostupné koncové body",
|
||||
"com_ui_enter": "Enter",
|
||||
"com_ui_enter_api_key": "Zadejte API klíč",
|
||||
"com_ui_enter_openapi_schema": "Zadejte svůj OpenAPI schéma zde",
|
||||
"com_ui_enter_var": "Zadejte {{0}}",
|
||||
"com_ui_error": "Chyba",
|
||||
"com_ui_error_connection": "Chyba při připojení k serveru, zkuste obnovit stránku.",
|
||||
"com_ui_error_save_admin_settings": "Při ukládání nastavení správce došlo k chybě.",
|
||||
"com_ui_examples": "Příklady",
|
||||
"com_ui_export_convo_modal": "Exportovat konverzaci",
|
||||
"com_ui_field_required": "Toto pole je povinné",
|
||||
"com_ui_filter_prompts": "Filtrovat výzvy",
|
||||
"com_ui_filter_prompts_name": "Filtrovat výzvy podle názvu",
|
||||
"com_ui_finance": "Finance",
|
||||
"com_ui_fork": "Rozdělit",
|
||||
"com_ui_fork_all_target": "Zahrnout vše od/do",
|
||||
"com_ui_fork_branches": "Zahrnout související větve",
|
||||
"com_ui_fork_change_default": "Výchozí možnost rozdělení",
|
||||
"com_ui_fork_default": "Použít výchozí možnost rozdělení",
|
||||
"com_ui_fork_error": "Při rozdělování konverzace došlo k chybě",
|
||||
"com_ui_fork_from_message": "Vyberte možnost rozdělení",
|
||||
"com_ui_fork_info_1": "Použijte toto nastavení pro rozdělení zpráv podle požadovaného chování.",
|
||||
"com_ui_fork_info_2": "\"Rozdělení\" znamená vytvoření nové konverzace začínající/končící u určitých zpráv v aktuální konverzaci, čímž se vytvoří kopie dle vybraných možností.",
|
||||
"com_ui_fork_info_3": "\"Cílová zpráva\" označuje buď zprávu, ze které bylo okno otevřeno, nebo pokud zaškrtnete \"{{0}}\", nejnovější zprávu v konverzaci.",
|
||||
"com_ui_fork_info_branches": "Tato možnost rozděluje viditelné zprávy spolu se souvisejícími větvemi; jinými slovy, přímou cestu k cílové zprávě včetně větví na této cestě.",
|
||||
"com_ui_fork_info_remember": "Zaškrtnutím si zapamatujete vybrané možnosti pro budoucí použití, což urychlí rozdělování konverzací.",
|
||||
"com_ui_fork_info_start": "Pokud zaškrtnuto, rozdělení začne od této zprávy až po nejnovější zprávu v konverzaci dle zvoleného chování.",
|
||||
"com_ui_fork_info_target": "Tato možnost rozděluje všechny zprávy vedoucí k cílové zprávě, včetně sousedních; jinými slovy, zahrnuje všechny větve zpráv.",
|
||||
"com_ui_fork_info_visible": "Tato možnost rozděluje pouze viditelné zprávy; jinými slovy, přímou cestu k cílové zprávě bez větví.",
|
||||
"com_ui_fork_processing": "Rozděluji konverzaci...",
|
||||
"com_ui_fork_remember": "Zapamatovat",
|
||||
"com_ui_fork_remember_checked": "Vaše volba bude zapamatována. Můžete ji kdykoli změnit v nastavení.",
|
||||
"com_ui_fork_split_target": "Začít rozdělení zde",
|
||||
"com_ui_fork_split_target_setting": "Výchozí rozdělení od cílové zprávy",
|
||||
"com_ui_fork_success": "Konverzace úspěšně rozdělena",
|
||||
"com_ui_fork_visible": "Pouze viditelné zprávy",
|
||||
"com_ui_generate_backup": "Generovat záložní kódy",
|
||||
"com_ui_generate_qrcode": "Generovat QR kód",
|
||||
"com_ui_generating": "Generuji...",
|
||||
"com_ui_global_group": "něco sem musí přijít. bylo prázdné",
|
||||
"com_ui_go_back": "Zpět",
|
||||
"com_ui_go_to_conversation": "Přejít na konverzaci",
|
||||
"com_ui_happy_birthday": "Mám 1. narozeniny!",
|
||||
"com_ui_hide_qr": "Skrýt QR kód",
|
||||
"com_ui_host": "Hostitel",
|
||||
"com_ui_idea": "Nápady",
|
||||
"com_ui_image_gen": "Generování obrázků",
|
||||
"com_ui_import": "Importovat",
|
||||
"com_ui_import_conversation_error": "Při importu konverzací došlo k chybě",
|
||||
"com_ui_import_conversation_file_type_error": "Nepodporovaný typ souboru pro import",
|
||||
"com_ui_import_conversation_info": "Importovat konverzace ze souboru JSON",
|
||||
"com_ui_import_conversation_success": "Konverzace úspěšně importovány",
|
||||
"com_ui_include_shadcnui": "Zahrnout instrukce pro shadcn/ui",
|
||||
"com_ui_input": "Vstup",
|
||||
"com_ui_instructions": "Instrukce",
|
||||
"com_ui_latest_footer": "AICon se může plést. Vždy kontrolujte důležité informace.",
|
||||
"com_ui_latest_production_version": "Nejnovější produkční verze",
|
||||
"com_ui_latest_version": "Nejnovější verze",
|
||||
"com_ui_librechat_code_api_key": "Získejte svůj API klíč pro LibreChat Code Interpreter",
|
||||
"com_ui_librechat_code_api_subtitle": "Bezpečné. Vícejazyčné. Vstupní/Výstupní soubory.",
|
||||
"com_ui_librechat_code_api_title": "Spustit AI kód",
|
||||
"com_ui_llm_menu": "Nabídka LLM",
|
||||
"com_ui_llms_available": "Dostupné LLM modely",
|
||||
"com_ui_loading": "Načítání...",
|
||||
"com_ui_locked": "Zamčeno",
|
||||
"com_ui_logo": "Logo {{0}}",
|
||||
"com_ui_manage": "Spravovat",
|
||||
"com_ui_max_tags": "Maximální povolený počet je {{0}}, používám nejnovější hodnoty.",
|
||||
"com_ui_mention": "Zmiňte koncový bod, asistenta nebo předvolbu pro rychlé přepnutí",
|
||||
"com_ui_min_tags": "Nelze odebrat další hodnoty, minimální počet je {{0}}.",
|
||||
"com_ui_misc": "Různé",
|
||||
"com_ui_model": "Model",
|
||||
"com_ui_model_parameters": "Parametry modelu",
|
||||
"com_ui_more_info": "Více informací",
|
||||
"com_ui_my_prompts": "Moje výzvy",
|
||||
"com_ui_name": "Název",
|
||||
"com_ui_new": "Nový",
|
||||
"com_ui_new_chat": "Nový chat",
|
||||
"com_ui_next": "Další",
|
||||
"com_ui_no": "Ne",
|
||||
"com_ui_no_backup_codes": "Nejsou k dispozici žádné záložní kódy. Vygenerujte nové.",
|
||||
"com_ui_no_bookmarks": "Zdá se, že zatím nemáte žádné záložky. Klikněte na chat a přidejte novou.",
|
||||
"com_ui_no_category": "Žádná kategorie",
|
||||
"com_ui_no_changes": "Žádné změny k aktualizaci",
|
||||
"com_ui_no_data": "něco sem musí přijít. bylo prázdné",
|
||||
"com_ui_no_terms_content": "Žádný obsah podmínek a pravidel k zobrazení",
|
||||
"com_ui_no_valid_items": "něco sem musí přijít. bylo prázdné",
|
||||
"com_ui_none": "Žádné",
|
||||
"com_ui_none_selected": "Nic nevybráno",
|
||||
"com_ui_not_used": "Nepoužito",
|
||||
"com_ui_nothing_found": "Nic nenalezeno",
|
||||
"com_ui_oauth": "OAuth",
|
||||
"com_ui_of": "z",
|
||||
"com_ui_off": "Vypnuto",
|
||||
"com_ui_on": "Zapnuto",
|
||||
"com_ui_openai": "OpenAI",
|
||||
"com_ui_page": "Stránka",
|
||||
"com_ui_prev": "Předchozí",
|
||||
"com_ui_preview": "Náhled",
|
||||
"com_ui_privacy_policy": "Zásady ochrany osobních údajů",
|
||||
"com_ui_privacy_policy_url": "URL zásad ochrany osobních údajů",
|
||||
"com_ui_prompt": "Výzva",
|
||||
"com_ui_prompt_already_shared_to_all": "Tato výzva je již sdílena se všemi uživateli",
|
||||
"com_ui_prompt_name": "Název výzvy",
|
||||
"com_ui_prompt_name_required": "Název výzvy je povinný",
|
||||
"com_ui_prompt_preview_not_shared": "Autor neumožnil spolupráci na této výzvě.",
|
||||
"com_ui_prompt_text": "Text",
|
||||
"com_ui_prompt_text_required": "Text je povinný",
|
||||
"com_ui_prompt_update_error": "Při aktualizaci výzvy došlo k chybě",
|
||||
"com_ui_prompts": "Výzvy",
|
||||
"com_ui_prompts_allow_create": "Povolit vytváření výzev",
|
||||
"com_ui_prompts_allow_share_global": "Povolit sdílení výzev všem uživatelům",
|
||||
"com_ui_prompts_allow_use": "Povolit používání výzev",
|
||||
"com_ui_provider": "Poskytovatel",
|
||||
"com_ui_read_aloud": "Přečíst nahlas",
|
||||
"com_ui_refresh_link": "Obnovit odkaz",
|
||||
"com_ui_regenerate": "Znovu vygenerovat",
|
||||
"com_ui_regenerate_backup": "Znovu vygenerovat záložní kódy",
|
||||
"com_ui_regenerating": "Generuji znovu...",
|
||||
"com_ui_region": "Oblast",
|
||||
"com_ui_rename": "Přejmenovat",
|
||||
"com_ui_rename_prompt": "Přejmenovat výzvu",
|
||||
"com_ui_requires_auth": "Vyžaduje ověření",
|
||||
"com_ui_reset_var": "Obnovit {{0}}",
|
||||
"com_ui_result": "Výsledek",
|
||||
"com_ui_revoke": "Odvolat",
|
||||
"com_ui_revoke_info": "Odvolat všechna uživatelem poskytnutá pověření",
|
||||
"com_ui_revoke_key_confirm": "Opravdu chcete odvolat tento klíč?",
|
||||
"com_ui_revoke_key_endpoint": "Odvolat klíč pro {{0}}",
|
||||
"com_ui_revoke_keys": "Odvolat klíče",
|
||||
"com_ui_revoke_keys_confirm": "Opravdu chcete odvolat všechny klíče?",
|
||||
"com_ui_role_select": "Role",
|
||||
"com_ui_roleplay": "Roleplay",
|
||||
"com_ui_run_code": "Spustit kód",
|
||||
"com_ui_run_code_error": "Při spouštění kódu došlo k chybě",
|
||||
"com_ui_save": "Uložit",
|
||||
"com_ui_save_submit": "Uložit a odeslat",
|
||||
"com_ui_saved": "Uloženo!",
|
||||
"com_ui_schema": "Schéma",
|
||||
"com_ui_scope": "Rozsah",
|
||||
"com_ui_search": "Hledat",
|
||||
"com_ui_secret_key": "Tajný klíč",
|
||||
"com_ui_select": "Vybrat",
|
||||
"com_ui_select_file": "Vyberte soubor",
|
||||
"com_ui_select_model": "Vyberte model",
|
||||
"com_ui_select_provider": "Vyberte poskytovatele",
|
||||
"com_ui_select_provider_first": "Nejprve vyberte poskytovatele",
|
||||
"com_ui_select_region": "Vyberte oblast",
|
||||
"com_ui_select_search_model": "Hledat model podle názvu",
|
||||
"com_ui_select_search_plugin": "Hledat plugin podle názvu",
|
||||
"com_ui_select_search_provider": "Hledat poskytovatele podle názvu",
|
||||
"com_ui_select_search_region": "Hledat oblast podle názvu",
|
||||
"com_ui_share": "Sdílet",
|
||||
"com_ui_share_create_message": "Vaše jméno a zprávy, které přidáte po sdílení, zůstanou soukromé.",
|
||||
"com_ui_share_delete_error": "Při mazání sdíleného odkazu došlo k chybě",
|
||||
"com_ui_share_error": "Při sdílení odkazu na chat došlo k chybě",
|
||||
"com_ui_share_form_description": "něco sem musí přijít. bylo prázdné",
|
||||
"com_ui_share_link_to_chat": "Sdílet odkaz na chat",
|
||||
"com_ui_share_to_all_users": "Sdílet se všemi uživateli",
|
||||
"com_ui_share_update_message": "Vaše jméno, vlastní instrukce a zprávy přidané po sdílení zůstanou soukromé.",
|
||||
"com_ui_share_var": "Sdílet {{0}}",
|
||||
"com_ui_shared_link_bulk_delete_success": "Sdílené odkazy byly úspěšně smazány",
|
||||
"com_ui_shared_link_delete_success": "Sdílený odkaz byl úspěšně smazán",
|
||||
"com_ui_shared_link_not_found": "Sdílený odkaz nebyl nalezen",
|
||||
"com_ui_shared_prompts": "Sdílené výzvy",
|
||||
"com_ui_shop": "Nakupování",
|
||||
"com_ui_show": "Zobrazit",
|
||||
"com_ui_show_all": "Zobrazit vše",
|
||||
"com_ui_show_qr": "Zobrazit QR kód",
|
||||
"com_ui_sign_in_to_domain": "Přihlásit se do {{0}}",
|
||||
"com_ui_simple": "Jednoduché",
|
||||
"com_ui_size": "Velikost",
|
||||
"com_ui_special_variables": "Speciální proměnné:",
|
||||
"com_ui_special_variables_info": "Použijte `{{current_date}}` pro aktuální datum a `{{current_user}}` pro vaše uživatelské jméno.",
|
||||
"com_ui_speech_while_submitting": "Nelze odeslat hlasový vstup, zatímco se generuje odpověď",
|
||||
"com_ui_stop": "Zastavit",
|
||||
"com_ui_storage": "Úložiště",
|
||||
"com_ui_submit": "Odeslat",
|
||||
"com_ui_teach_or_explain": "Učení",
|
||||
"com_ui_temporary_chat": "Dočasný chat",
|
||||
"com_ui_terms_and_conditions": "Obchodní podmínky",
|
||||
"com_ui_terms_of_service": "Podmínky služby",
|
||||
"com_ui_thinking": "Přemýšlení...",
|
||||
"com_ui_thoughts": "Myšlenky",
|
||||
"com_ui_token_exchange_method": "Metoda výměny tokenů",
|
||||
"com_ui_token_url": "URL tokenu",
|
||||
"com_ui_tools": "Nástroje",
|
||||
"com_ui_travel": "Cestování",
|
||||
"com_ui_unarchive": "Obnovit archiv",
|
||||
"com_ui_unarchive_error": "Nepodařilo se obnovit archivovanou konverzaci",
|
||||
"com_ui_unknown": "Neznámé",
|
||||
"com_ui_update": "Aktualizovat",
|
||||
"com_ui_upload": "Nahrát",
|
||||
"com_ui_upload_code_files": "Nahrát soubory pro interpret kódu",
|
||||
"com_ui_upload_delay": "Nahrávání \"{{0}}\" trvá déle než obvykle. Počkejte, než bude soubor indexován.",
|
||||
"com_ui_upload_error": "Při nahrávání souboru došlo k chybě",
|
||||
"com_ui_upload_file_search": "Nahrát pro vyhledávání v souborech",
|
||||
"com_ui_upload_files": "Nahrát soubory",
|
||||
"com_ui_upload_image": "Nahrát obrázek",
|
||||
"com_ui_upload_image_input": "Nahrát obrázek",
|
||||
"com_ui_upload_invalid": "Neplatný soubor pro nahrání. Musí to být obrázek nepřesahující limit.",
|
||||
"com_ui_upload_invalid_var": "Neplatný soubor pro nahrání. Musí to být obrázek nepřesahující {{0}} MB",
|
||||
"com_ui_upload_success": "Soubor byl úspěšně nahrán",
|
||||
"com_ui_upload_type": "Vyberte typ nahrávání",
|
||||
"com_ui_use_2fa_code": "Použít kód 2FA",
|
||||
"com_ui_use_backup_code": "Použít záložní kód",
|
||||
"com_ui_use_micrphone": "Použít mikrofon",
|
||||
"com_ui_use_prompt": "Použít výzvu",
|
||||
"com_ui_used": "Použito",
|
||||
"com_ui_variables": "Proměnné",
|
||||
"com_ui_variables_info": "Použijte dvojité složené závorky k vytvoření proměnných, např. `{{příklad proměnné}}`, které lze vyplnit při použití výzvy.",
|
||||
"com_ui_verify": "Ověřit",
|
||||
"com_ui_version_var": "Verze {{0}}",
|
||||
"com_ui_versions": "Verze",
|
||||
"com_ui_view_source": "Zobrazit zdrojový chat",
|
||||
"com_ui_write": "Psát",
|
||||
"com_ui_yes": "Ano",
|
||||
"com_ui_zoom": "Přiblížit",
|
||||
"com_user_message": "Vy",
|
||||
"com_warning_resubmit_unsupported": "Opětovné odeslání AI zprávy není pro tento koncový bod podporováno."
|
||||
}
|
||||
@@ -191,7 +191,7 @@
|
||||
"com_endpoint_instructions_assistants_placeholder": "Überschreibt die Anweisungen des Assistenten. Dies ist nützlich, um das Verhalten auf Basis einzelner Ausführungen zu modifizieren.",
|
||||
"com_endpoint_max_output_tokens": "Max. Antwort-Token",
|
||||
"com_endpoint_message": "Nachricht an",
|
||||
"com_endpoint_message_new": "Nachricht {{0}}",
|
||||
"com_endpoint_message_new": "Nachricht an {{0}}",
|
||||
"com_endpoint_message_not_appendable": "Bearbeite deine Nachricht oder generiere neu.",
|
||||
"com_endpoint_my_preset": "Meine Voreinstellung",
|
||||
"com_endpoint_no_presets": "Noch keine Voreinstellungen, verwende die KI-Einstellungsschaltfläche, um eine zu erstellen",
|
||||
@@ -353,6 +353,7 @@
|
||||
"com_nav_lang_estonian": "Eesti keel",
|
||||
"com_nav_lang_finnish": "Suomi",
|
||||
"com_nav_lang_french": "Français ",
|
||||
"com_nav_lang_georgian": "ქართული",
|
||||
"com_nav_lang_german": "Deutsch",
|
||||
"com_nav_lang_hebrew": "עברית",
|
||||
"com_nav_lang_indonesia": "Indonesia",
|
||||
@@ -364,6 +365,7 @@
|
||||
"com_nav_lang_russian": "Русский",
|
||||
"com_nav_lang_spanish": "Español",
|
||||
"com_nav_lang_swedish": "Svenska",
|
||||
"com_nav_lang_thai": "ไทย",
|
||||
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||
"com_nav_lang_turkish": "Türkçe",
|
||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||
@@ -597,6 +599,7 @@
|
||||
"com_ui_download_backup": "Backup-Codes herunterladen",
|
||||
"com_ui_download_backup_tooltip": "Bevor Sie fortfahren, laden Sie bitte Ihre Backup-Codes herunter. Sie benötigen sie, um den Zugang wiederherzustellen, falls Sie Ihr Authentifizierungsgerät verlieren.",
|
||||
"com_ui_download_error": "Fehler beim Herunterladen der Datei. Die Datei wurde möglicherweise gelöscht.",
|
||||
"com_ui_drag_drop": "Ziehen und Ablegen",
|
||||
"com_ui_dropdown_variables": "Dropdown-Variablen:",
|
||||
"com_ui_dropdown_variables_info": "Erstellen Sie benutzerdefinierte Dropdown-Menüs für Ihre Eingabeaufforderungen: `{{variable_name:option1|option2|option3}}`",
|
||||
"com_ui_duplicate": "Duplizieren",
|
||||
@@ -604,6 +607,7 @@
|
||||
"com_ui_duplication_processing": "Konversation wird dupliziert...",
|
||||
"com_ui_duplication_success": "Unterhaltung erfolgreich dupliziert",
|
||||
"com_ui_edit": "Bearbeiten",
|
||||
"com_ui_empty_category": "-",
|
||||
"com_ui_endpoint": "Endpunkt",
|
||||
"com_ui_endpoint_menu": "LLM-Endpunkt-Menü",
|
||||
"com_ui_endpoints_available": "Verfügbare Endpunkte",
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"com_agents_create_error": "There was an error creating your agent.",
|
||||
"com_agents_description_placeholder": "Optional: Describe your Agent here",
|
||||
"com_agents_enable_file_search": "Enable File Search",
|
||||
"com_agents_file_context": "File Context (OCR)",
|
||||
"com_agents_file_context_disabled": "Agent must be created before uploading files for File Context.",
|
||||
"com_agents_file_context_info": "Files uploaded as \"Context\" are processed using OCR to extract text, which is then added to the Agent's instructions. Ideal for documents, images with text, or PDFs where you need the full text content of a file",
|
||||
"com_agents_file_search_disabled": "Agent must be created before uploading files for 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_instructions_placeholder": "The system instructions that the agent uses",
|
||||
@@ -369,6 +372,7 @@
|
||||
"com_nav_lang_russian": "Русский",
|
||||
"com_nav_lang_spanish": "Español",
|
||||
"com_nav_lang_swedish": "Svenska",
|
||||
"com_nav_lang_thai": "ไทย",
|
||||
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||
"com_nav_lang_turkish": "Türkçe",
|
||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||
@@ -810,10 +814,12 @@
|
||||
"com_ui_upload_code_files": "Upload for Code Interpreter",
|
||||
"com_ui_upload_delay": "Uploading \"{{0}}\" is taking more time than anticipated. Please wait while the file finishes indexing for retrieval.",
|
||||
"com_ui_upload_error": "There was an error uploading your file",
|
||||
"com_ui_upload_file_context": "Upload File Context",
|
||||
"com_ui_upload_file_search": "Upload for File Search",
|
||||
"com_ui_upload_files": "Upload files",
|
||||
"com_ui_upload_image": "Upload an image",
|
||||
"com_ui_upload_image_input": "Upload Image",
|
||||
"com_ui_upload_ocr_text": "Upload as Text",
|
||||
"com_ui_upload_invalid": "Invalid file for upload. Must be an image not exceeding the limit",
|
||||
"com_ui_upload_invalid_var": "Invalid file for upload. Must be an image not exceeding {{0}} MB",
|
||||
"com_ui_upload_success": "Successfully uploaded file",
|
||||
@@ -834,4 +840,4 @@
|
||||
"com_ui_zoom": "Zoom",
|
||||
"com_user_message": "You",
|
||||
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user