Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔥🚀 feat: CDN (Firebase) & feat: account section #1438

Merged
merged 35 commits into from
Dec 30, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
fc44b47
localization + api-endpoint
berry-13 Dec 25, 2023
25954a6
docs: added firebase documentation
berry-13 Dec 25, 2023
8d077c5
chore: icons
berry-13 Dec 25, 2023
e76c8d4
chore: SettingsTabs
berry-13 Dec 25, 2023
095564b
feat: account pannel; fix: gear icons
berry-13 Dec 25, 2023
06b9138
docs: position update
berry-13 Dec 25, 2023
222475a
feat: firebase
berry-13 Dec 25, 2023
4ce1004
feat: plugin support
berry-13 Dec 25, 2023
13e71b7
route
berry-13 Dec 25, 2023
27ae0eb
fixed bugs with firebase and moved a lot of files
berry-13 Dec 26, 2023
ee6ea90
chore(DALLE3): using UUID v4
berry-13 Dec 26, 2023
fea4d13
feat: support for social strategies; moved '/images' path
berry-13 Dec 26, 2023
bb19db7
fix: data ignored
berry-13 Dec 27, 2023
3a22490
gitignore update
berry-13 Dec 27, 2023
316aa08
Merge branch 'main' into firebase-cdn-new
berry-13 Dec 28, 2023
cd2c156
docs: update firebase guide
danny-avila Dec 29, 2023
b62dabc
refactor: Firebase
danny-avila Dec 29, 2023
ab5f901
ci(DALLE/DALLE3): fix tests to use logger and new expected outputs, a…
danny-avila Dec 29, 2023
677b6cd
refactor(loadToolWithAuth): pass userId to tool as field
danny-avila Dec 29, 2023
abd9d9d
feat(images/parse): feat: Add URL Image Basename Extraction
danny-avila Dec 29, 2023
011d640
refactor(addImages): function to use a more specific regular expressi…
danny-avila Dec 29, 2023
b10d357
refactor(DALLE/DALLE3): utilize `getImageBasename` and `this.userId`;…
danny-avila Dec 29, 2023
4f73e60
fix(addImages): make more general to match any image markdown descriptor
danny-avila Dec 29, 2023
1f09465
fix(parse/getImageBasename): test result of this function for an actu…
danny-avila Dec 29, 2023
a1ecdb4
ci(DALLE3): mock getImageBasename
danny-avila Dec 29, 2023
27f22a4
refactor(AuthContext): use Recoil atom state for user
danny-avila Dec 29, 2023
6d6a352
feat: useUploadAvatarMutation, react-query hook for avatar upload
danny-avila Dec 29, 2023
ac22be9
fix(Toast): stack z-order of Toast over all components (1000)
danny-avila Dec 29, 2023
9fae88a
refactor(showToast): add optional status field to avoid importing Not…
danny-avila Dec 29, 2023
520c012
refactor(routes/avatar): remove unnecessary get route, get userId fro…
danny-avila Dec 29, 2023
bd7699f
chore(uploadAvatar): TODO: remove direct use of Model, `User`
danny-avila Dec 29, 2023
5a5c69d
fix(client): fix Spinner imports
danny-avila Dec 29, 2023
ac35e3a
refactor(Avatar): use react-query hook, Toast, remove unnecessary sta…
danny-avila Dec 29, 2023
f572eeb
fix(avatar/localStrategy): correctly save local profile picture and c…
danny-avila Dec 29, 2023
d2458e0
fix: use `includes` instead of `endsWith` for checking manual query o…
danny-avila Dec 29, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,17 @@ EMAIL_PASSWORD=
EMAIL_FROM_NAME=
EMAIL_FROM=noreply@librechat.ai

#========================#
# Firebase CDN #
#========================#

FIREBASE_API_KEY=
FIREBASE_AUTH_DOMAIN=
FIREBASE_PROJECT_ID=
FIREBASE_STORAGE_BUCKET=
FIREBASE_MESSAGING_SENDER_ID=
FIREBASE_APP_ID=

#==================================================#
# Others #
#==================================================#
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,6 @@ data.ms/*
auth.json

/packages/ux-shared/
/images
/images

!client/src/components/Nav/SettingsTabs/Data/
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reorganized all the settings tabs, but it seems that "data/" is being ignored. Therefore, I added !client/src/components/Nav/SettingsTabs/Data/ to include Data.tsx in commits

6 changes: 2 additions & 4 deletions api/app/clients/output_parsers/addImages.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,10 @@ function addImages(intermediateSteps, responseMessage) {
if (!observation || !observation.includes('![')) {
return;
}
const observedImagePath = observation.match(/\(\/images\/.*\.\w*\)/g);
const observedImagePath = observation.match(/!\[.*\]\([^)]*\)/g);
if (observedImagePath && !responseMessage.text.includes(observedImagePath[0])) {
responseMessage.text += '\n' + observation;
if (process.env.DEBUG_PLUGINS) {
logger.debug('[addImages] added image from intermediateSteps:', observation);
}
logger.debug('[addImages] added image from intermediateSteps:', observation);
}
});
}
Expand Down
57 changes: 43 additions & 14 deletions api/app/clients/tools/DALL-E.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ const fs = require('fs');
const path = require('path');
const OpenAI = require('openai');
// const { genAzureEndpoint } = require('~/utils/genAzureEndpoints');
const { v4: uuidv4 } = require('uuid');
const { Tool } = require('langchain/tools');
const { HttpsProxyAgent } = require('https-proxy-agent');
const {
saveImageToFirebaseStorage,
getFirebaseStorageImageUrl,
getFirebaseStorage,
} = require('~/server/services/Files/Firebase');
const { getImageBasename } = require('~/server/services/Files/images');
const extractBaseURL = require('~/utils/extractBaseURL');
const saveImageFromUrl = require('./saveImageFromUrl');
const { logger } = require('~/config');
Expand All @@ -15,7 +22,9 @@ class OpenAICreateImage extends Tool {
constructor(fields = {}) {
super();

this.userId = fields.userId;
let apiKey = fields.DALLE_API_KEY || this.getApiKey();

const config = { apiKey };
if (DALLE_REVERSE_PROXY) {
config.baseURL = extractBaseURL(DALLE_REVERSE_PROXY);
Expand All @@ -24,7 +33,6 @@ class OpenAICreateImage extends Tool {
if (PROXY) {
config.httpAgent = new HttpsProxyAgent(PROXY);
}

// let azureKey = fields.AZURE_API_KEY || process.env.AZURE_API_KEY;

// if (azureKey) {
Expand Down Expand Up @@ -97,12 +105,11 @@ Guidelines:
throw new Error('No image URL returned from OpenAI API.');
}

const regex = /img-[\w\d]+.png/;
const match = theImageUrl.match(regex);
let imageName = '1.png';
const imageBasename = getImageBasename(theImageUrl);
let imageName = `image_${uuidv4()}.png`;

if (match) {
imageName = match[0];
if (imageBasename) {
imageName = imageBasename;
logger.debug('[DALL-E]', { imageName }); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png
} else {
logger.debug('[DALL-E] No image name found in the string.', {
Expand All @@ -111,7 +118,18 @@ Guidelines:
});
}

this.outputPath = path.resolve(__dirname, '..', '..', '..', '..', 'client', 'public', 'images');
this.outputPath = path.resolve(
__dirname,
'..',
'..',
'..',
'..',
'client',
'public',
'images',
this.userId,
);

const appRoot = path.resolve(__dirname, '..', '..', '..', '..', 'client');
this.relativeImageUrl = path.relative(appRoot, this.outputPath);

Expand All @@ -120,14 +138,25 @@ Guidelines:
fs.mkdirSync(this.outputPath, { recursive: true });
}

try {
await saveImageFromUrl(theImageUrl, this.outputPath, imageName);
this.result = this.getMarkdownImageUrl(imageName);
} catch (error) {
logger.error('Error while saving the DALL-E image:', error);
this.result = theImageUrl;
const storage = getFirebaseStorage();
if (storage) {
try {
await saveImageToFirebaseStorage(this.userId, theImageUrl, imageName);
this.result = await getFirebaseStorageImageUrl(`${this.userId}/${imageName}`);
logger.debug('[DALL-E] result: ' + this.result);
} catch (error) {
logger.error('Error while saving the image to Firebase Storage:', error);
this.result = `Failed to save the image to Firebase Storage. ${error.message}`;
}
} else {
try {
await saveImageFromUrl(theImageUrl, this.outputPath, imageName);
this.result = this.getMarkdownImageUrl(imageName);
} catch (error) {
logger.error('Error while saving the image locally:', error);
this.result = `Failed to save the image locally. ${error.message}`;
}
}

return this.result;
}
}
Expand Down
49 changes: 34 additions & 15 deletions api/app/clients/tools/structured/DALLE3.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@ const fs = require('fs');
const path = require('path');
const { z } = require('zod');
const OpenAI = require('openai');
const { v4: uuidv4 } = require('uuid');
const { Tool } = require('langchain/tools');
const { HttpsProxyAgent } = require('https-proxy-agent');
const saveImageFromUrl = require('../saveImageFromUrl');
const {
saveImageToFirebaseStorage,
getFirebaseStorageImageUrl,
getFirebaseStorage,
} = require('~/server/services/Files/Firebase');
const { getImageBasename } = require('~/server/services/Files/images');
const extractBaseURL = require('~/utils/extractBaseURL');
const saveImageFromUrl = require('../saveImageFromUrl');
const { logger } = require('~/config');

const { DALLE3_SYSTEM_PROMPT, DALLE_REVERSE_PROXY, PROXY } = process.env;
class DALLE3 extends Tool {
constructor(fields = {}) {
super();

this.userId = fields.userId;
let apiKey = fields.DALLE_API_KEY || this.getApiKey();
const config = { apiKey };
if (DALLE_REVERSE_PROXY) {
Expand Down Expand Up @@ -108,12 +116,12 @@ class DALLE3 extends Tool {
n: 1,
});
} catch (error) {
return `Something went wrong when trying to generate the image. The DALL-E API may unavailable:
return `Something went wrong when trying to generate the image. The DALL-E API may be unavailable:
Error Message: ${error.message}`;
}

if (!resp) {
return 'Something went wrong when trying to generate the image. The DALL-E API may unavailable';
return 'Something went wrong when trying to generate the image. The DALL-E API may be unavailable';
}

const theImageUrl = resp.data[0].url;
Expand All @@ -122,12 +130,11 @@ Error Message: ${error.message}`;
return 'No image URL returned from OpenAI API. There may be a problem with the API or your configuration.';
}

const regex = /img-[\w\d]+.png/;
const match = theImageUrl.match(regex);
let imageName = '1.png';
const imageBasename = getImageBasename(theImageUrl);
let imageName = `image_${uuidv4()}.png`;

if (match) {
imageName = match[0];
if (imageBasename) {
imageName = imageBasename;
logger.debug('[DALL-E-3]', { imageName }); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png
} else {
logger.debug('[DALL-E-3] No image name found in the string.', {
Expand All @@ -146,6 +153,7 @@ Error Message: ${error.message}`;
'client',
'public',
'images',
this.userId,
);
const appRoot = path.resolve(__dirname, '..', '..', '..', '..', '..', 'client');
this.relativeImageUrl = path.relative(appRoot, this.outputPath);
Expand All @@ -154,13 +162,24 @@ Error Message: ${error.message}`;
if (!fs.existsSync(this.outputPath)) {
fs.mkdirSync(this.outputPath, { recursive: true });
}

try {
await saveImageFromUrl(theImageUrl, this.outputPath, imageName);
this.result = this.getMarkdownImageUrl(imageName);
} catch (error) {
logger.error('Error while saving the image:', error);
this.result = theImageUrl;
const storage = getFirebaseStorage();
if (storage) {
try {
await saveImageToFirebaseStorage(this.userId, theImageUrl, imageName);
this.result = await getFirebaseStorageImageUrl(`${this.userId}/${imageName}`);
logger.debug('[DALL-E-3] result: ' + this.result);
} catch (error) {
logger.error('Error while saving the image to Firebase Storage:', error);
this.result = `Failed to save the image to Firebase Storage. ${error.message}`;
}
} else {
try {
await saveImageFromUrl(theImageUrl, this.outputPath, imageName);
this.result = this.getMarkdownImageUrl(imageName);
} catch (error) {
logger.error('Error while saving the image locally:', error);
this.result = `Failed to save the image locally. ${error.message}`;
}
}

return this.result;
Expand Down
74 changes: 72 additions & 2 deletions api/app/clients/tools/structured/specs/DALLE3.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,40 @@ const fs = require('fs');
const path = require('path');
const OpenAI = require('openai');
const DALLE3 = require('../DALLE3');
const {
getFirebaseStorage,
saveImageToFirebaseStorage,
} = require('~/server/services/Files/Firebase');
const saveImageFromUrl = require('../../saveImageFromUrl');
const { logger } = require('~/config');

jest.mock('openai');

jest.mock('~/server/services/Files/Firebase', () => ({
getFirebaseStorage: jest.fn(),
saveImageToFirebaseStorage: jest.fn(),
getFirebaseStorageImageUrl: jest.fn(),
}));

jest.mock('~/server/services/Files/images', () => ({
getImageBasename: jest.fn().mockImplementation((url) => {
// Split the URL by '/'
const parts = url.split('/');

// Get the last part of the URL
const lastPart = parts.pop();

// Check if the last part of the URL matches the image extension regex
const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i;
if (imageExtensionRegex.test(lastPart)) {
return lastPart;
}

// If the regex test fails, return an empty string
return '';
}),
}));

const generate = jest.fn();
OpenAI.mockImplementation(() => ({
images: {
Expand Down Expand Up @@ -187,7 +216,48 @@ describe('DALLE3', () => {
generate.mockResolvedValue(mockResponse);
saveImageFromUrl.mockRejectedValue(error);
const result = await dalle._call(mockData);
expect(logger.error).toHaveBeenCalledWith('Error while saving the image:', error);
expect(result).toBe(mockResponse.data[0].url);
expect(logger.error).toHaveBeenCalledWith('Error while saving the image locally:', error);
expect(result).toBe('Failed to save the image locally. Error while saving the image');
});

it('should save image to Firebase Storage if Firebase is initialized', async () => {
const mockData = {
prompt: 'A test prompt',
};
const mockImageUrl = 'http://example.com/img-test.png';
const mockResponse = { data: [{ url: mockImageUrl }] };
generate.mockResolvedValue(mockResponse);
getFirebaseStorage.mockReturnValue({}); // Simulate Firebase being initialized

await dalle._call(mockData);

expect(getFirebaseStorage).toHaveBeenCalled();
expect(saveImageToFirebaseStorage).toHaveBeenCalledWith(
undefined,
mockImageUrl,
expect.any(String),
);
});

it('should handle error when saving image to Firebase Storage fails', async () => {
const mockData = {
prompt: 'A test prompt',
};
const mockImageUrl = 'http://example.com/img-test.png';
const mockResponse = { data: [{ url: mockImageUrl }] };
const error = new Error('Error while saving to Firebase');
generate.mockResolvedValue(mockResponse);
getFirebaseStorage.mockReturnValue({}); // Simulate Firebase being initialized
saveImageToFirebaseStorage.mockRejectedValue(error);

const result = await dalle._call(mockData);

expect(logger.error).toHaveBeenCalledWith(
'Error while saving the image to Firebase Storage:',
error,
);
expect(result).toBe(
'Failed to save the image to Firebase Storage. Error while saving to Firebase',
);
});
});
6 changes: 3 additions & 3 deletions api/app/clients/tools/util/handleTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,19 @@ const validateTools = async (user, tools = []) => {
}
};

const loadToolWithAuth = async (user, authFields, ToolConstructor, options = {}) => {
const loadToolWithAuth = async (userId, authFields, ToolConstructor, options = {}) => {
return async function () {
let authValues = {};

for (const authField of authFields) {
let authValue = process.env[authField];
if (!authValue) {
authValue = await getUserPluginAuthValue(user, authField);
authValue = await getUserPluginAuthValue(userId, authField);
}
authValues[authField] = authValue;
}

return new ToolConstructor({ ...options, ...authValues });
return new ToolConstructor({ ...options, ...authValues, userId });
};
};

Expand Down
1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"express-mongo-sanitize": "^2.2.0",
"express-rate-limit": "^6.9.0",
"express-session": "^1.17.3",
"firebase": "^10.6.0",
"googleapis": "^126.0.1",
"handlebars": "^4.7.7",
"html": "^1.0.0",
Expand Down
2 changes: 2 additions & 0 deletions api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const cors = require('cors');
const express = require('express');
const passport = require('passport');
const mongoSanitize = require('express-mongo-sanitize');
const { initializeFirebase } = require('~/server/services/Files/Firebase/initialize');
const errorController = require('./controllers/ErrorController');
const configureSocialLogins = require('./socialLogins');
const { connectDb, indexSync } = require('~/lib/db');
Expand All @@ -22,6 +23,7 @@ const { jwtLogin, passportLogin } = require('~/strategies');
const startServer = async () => {
await connectDb();
logger.info('Connected to MongoDB');
initializeFirebase();
await indexSync();

const app = express();
Expand Down
Loading